45 Commits

Author SHA1 Message Date
melod1n 7e696ec8c8 chore: adjust github workflows 2025-05-11 21:33:37 +03:00
melod1n b184d98670 Bump project versionName (#166) 2025-05-11 21:19:49 +03:00
melod1n 43539139e8 Simple attachments in messages history (#164)
* new attachments in messages history - photo, video, audio, file, link
* improve attachments in messages history and adjusted font size for logo's text in auth screen
* audio duration, file preview and url preview are now visible in attachments in messages history screen
* make MessageBubble width adapt to attachments container width
* topbar back icon crossfade animation
* implement rich text for message input
* handle click and long click on attachments
* added click and long click handlers for attachments in message bubbles
* enabled opening photos, files, and links when clicked.
* implemented basic long-click logging for photos.
* handled back press to return to Conversations from other tabs.
* corrected the logic for filtering and selecting video images.
* updated string resources for attachments, including a new "Clip" string.
* make MessageBubble mention text underline on out messages
2025-05-10 03:10:07 +03:00
melod1n f45a106ed8 Update build.yml 2025-05-09 05:11:57 +03:00
melod1n 8b31e88caf shorten texts in PinnedMessageContainer 2025-04-11 06:43:00 +03:00
dependabot[bot] 4cb34327cf Bump androidx.compose:compose-bom from 2025.03.01 to 2025.04.00 (#156) 2025-04-11 01:55:17 +00:00
dependabot[bot] c7b414b9f0 Bump room from 2.6.1 to 2.7.0 (#154) 2025-04-11 01:55:14 +00:00
dependabot[bot] 9a296c8b84 Bump coroutines from 1.10.1 to 1.10.2 (#153) 2025-04-11 01:55:08 +00:00
melod1n ca569354fb Bump app version 2025-04-04 21:51:41 +03:00
melod1n 89748b72ed Update API version (#147)
* Bump VK Api version to 5.238
* Implemented new authorization flow (at the moment, without auto re-requesting token)
* Add support for sticker pack preview attachments
* Bump LongPoll to version 19
* Improved messages handling
* Fixed coloring issues
* Cache improvements
* Archive screen with full functionality
* Recomposition fixes
* Markdown support for messages bubbles
* Adjust app name font size based on screen width
* Navigation related improvements
* Add logout functionality
2025-04-04 20:43:59 +03:00
dependabot[bot] add67b6f8d Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#149) 2025-04-01 17:46:12 +00:00
dependabot[bot] 25e7e39ed0 Bump koin from 4.0.3 to 4.0.4 (#148) 2025-04-01 17:45:15 +00:00
melod1n 935d313257 improve long polling service reliability 2025-03-30 19:54:12 +03:00
melod1n 5b5e8f8446 Implement scroll to top in friends and conversations screens 2025-03-29 22:31:48 +03:00
melod1n f1892670da more blur 2025-03-29 22:10:47 +03:00
melod1n d46c72f7e6 improve login screen UI and logic & fixes for blur 2025-03-29 22:03:37 +03:00
melod1n 157c0c71fe Update README.md 2025-03-29 03:02:00 +03:00
melod1n 988da07852 Fix deleting unsent messages and disable "for everyone" delete option 2025-03-29 03:00:50 +03:00
melod1n f02822a011 a shit ton features, improvements and fixes in messages history screen and others 2025-03-29 02:51:49 +03:00
melod1n da9644cde1 Add theme option to disable animations and fix account avatar loading in bottom bar 2025-03-28 19:59:38 +03:00
melod1n 9aa85d40c6 pinned message in messages history draft 2025-03-27 12:16:26 +03:00
melod1n f66123ba94 some fixes for pinned message 2025-03-27 04:54:30 +03:00
melod1n b80babed9c draft pinned message and fixes 2025-03-27 04:32:11 +03:00
melod1n 85c5a10891 update README.md 2025-03-27 03:51:07 +03:00
melod1n 51356aa4dd chat materials pagination and ui improvements 2025-03-27 03:50:39 +03:00
melod1n 807c23926e reworked chat materials screen and some fixes 2025-03-27 02:27:19 +03:00
melod1n 37a654790c Bump compose-bom from 2025.03.00 to 2025.03.01 2025-03-26 22:10:44 +03:00
dependabot[bot] a36060654d Bump koin from 4.0.2 to 4.0.3 (#145) 2025-03-26 19:08:57 +00:00
dependabot[bot] da5fa8d77a Bump ksp from 2.1.20-1.0.31 to 2.1.20-1.0.32 (#146) 2025-03-26 19:08:52 +00:00
melod1n 4d18c86f04 feat(messages): add message selection and actions 2025-03-26 22:06:55 +03:00
melod1n 296c3ce7a0 update README.md 2025-03-26 01:31:03 +03:00
melod1n 0ae05709db feat: Add ordering functionality for friends list 2025-03-26 01:28:50 +03:00
dependabot[bot] 3dbf2bd8a4 Bump com.google.guava:guava from 33.4.5-jre to 33.4.6-jre (#143) 2025-03-25 19:44:09 +00:00
dependabot[bot] 3b02e2ff61 Bump agp from 8.9.0 to 8.9.1 (#141) 2025-03-25 08:22:25 +00:00
dependabot[bot] b21675d6f2 Bump haze from 1.5.1 to 1.5.2 (#142) 2025-03-25 08:22:11 +00:00
melod1n 9e301af076 update gh actions' jdk 2025-03-23 20:54:07 +03:00
melod1n 3fdb574971 some updates 2025-03-23 20:51:15 +03:00
melod1n ad6e413bbb Update README.md 2025-03-23 20:00:13 +03:00
melod1n 8dc47c3fa5 separated screens for friends tab 2025-03-23 19:53:58 +03:00
melod1n 0eb3146428 Release 0.1.9 (#140)
* improvements in longpoll's stuff
2025-03-23 17:55:28 +03:00
melod1n b2879d8756 Release 0.1.8 (#139)
* pagination in chat fixed
* other fixes and improvements

* fixed visual bug in progress bar in chat history

* Refactor: Enhance conversations and friends features

-   In `ConversationsScreen`, removed `isNeedToScrollToTop` and `onScrolledToTop`, and refactored toolbar container color logic. Added `NoItemsView` for empty conversation lists.
-   In `MainGraph`, added `onMessageClicked` for navigation to message history.
-   In `ApiEvent`, introduced `parseOrNull` for handling unknown event types.
-   In `ConversationsViewModel`, removed `scrollToTop` logic and refactored error handling.
-   In `FriendsViewModel`, refactored error handling and introduced `onErrorConsumed` and `handleError`.
-   In `FriendItem`, added an icon button to initiate sending a message to a friend.
-   In `strings.xml`, added or updated strings for session expiration, log out, refreshing, and empty friend lists.
-   In `RootScreen`, added `onMessageClicked` for navigating to messages.
-   In `FriendsList`, added `onMessageClicked` for handling message clicks.
-   In `MainScreen`, removed unused `MutableSharedFlow`.
-   In `FriendsScreen`, added support for showing errors, added `onMessageClicked`, and replaced `hazeChild` with `hazeEffect` and `hazeSource`.
-   In `FriendsNavigation`, added `onMessageClicked` for handling message clicks.
-   In `ConversationsNavigation`, removed the unused `scrollToTopFlow` parameter.
-   In `ErrorView`, added text alignment.
-   In `NoItemsView`, added support for a button and custom text.
-   In `LongPollUpdatesParser`, replaced try-catch with `parseOrNull`.

* Chat creation feature (#138)

* - read indicator, edit status and time for message in messages history

* message sending status
2025-03-23 09:22:41 +03:00
melod1n 30e132d418 Release 0.1.7 (#136)
* Bump haze from 1.1.1 to 1.2.0 (#105)

* Bump org.jetbrains.kotlinx:kotlinx-serialization-json from 1.7.3 to 1.8.0 (#104)

* update gradle wrapper

* Bump agp from 8.7.3 to 8.8.0 (#106)

* Bump com.jraska.module.graph.assertion from 2.7.1 to 2.7.3 (#109)

* Bump haze from 1.2.0 to 1.2.2 (#111)

* Bump koin from 4.0.1 to 4.0.2 (#112)

* little improvement

* Bump kotlin from 2.1.0 to 2.1.10 (#113)

* Bump androidx.compose:compose-bom from 2024.12.01 to 2025.02.00 (#115)

* Bump androidx.navigation:navigation-compose from 2.8.5 to 2.8.7 (#119)

* Bump haze from 1.2.2 to 1.3.1 (#118)

* Bump ksp from 2.1.0-1.0.29 to 2.1.10-1.0.30 (#116)

* Bump agp from 8.8.0 to 8.8.1 (#117)

* Bump com.google.accompanist:accompanist-permissions (#121)

* Rename the app's namespace and applicationId to `dev.meloda.fastvk`, and update the package name in `ACTION_MANAGE_UNKNOWN_APP_SOURCES` intent. Remove unnecessary `onLowMemory` method in the `OnlineService`.

* Bump com.jraska.module.graph.assertion from 2.7.3 to 2.8.0 (#126)

* Bump ksp from 2.1.10-1.0.30 to 2.1.10-1.0.31 (#125)

* Bump haze from 1.3.1 to 1.4.0 (#124)

* Bump agp from 8.8.1 to 8.8.2 (#123)

* Bump androidx.navigation:navigation-compose from 2.8.7 to 2.8.8 (#122)

* Bump haze from 1.4.0 to 1.5.0 (#128)

* Bump agp from 8.8.2 to 8.9.0 (#127)

* Bump androidx.navigation:navigation-compose from 2.8.8 to 2.8.9 (#130)

* Bump androidx.compose:compose-bom from 2025.02.00 to 2025.03.00 (#129)

* revert agp version to 8.8.2

* fix issues with package names

* Bump haze from 1.5.0 to 1.5.1 (#133)

* Bump com.google.guava:guava from 33.4.0-jre to 33.4.5-jre (#132)

* russian translations

* fixes and improvements

---------

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 03:13:17 +03:00
melod1n 7e5843759d release/0.1.6 (#103)
* Bump com.google.guava:guava from 33.3.1-jre to 33.4.0-jre (#97)

* Bump coroutines from 1.9.0 to 1.10.1 (#100)

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

* Bump koin from 4.0.0 to 4.0.1 (#101)

* minor update

---------

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-11 06:38:08 +03:00
melod1n 7c14df1824 release 0.1.5 (#98)
* settings reorganization;
implement long press on emoji button for fast text;
some deprecations fixed;
some typos fixed;
etc

* ability to use more animations (experimental);
fix online friends loading;
conversation avatar in messages history screen;
test second tap on conversations item in bottom bar to scroll to top;
etc

* version up
2024-12-17 21:07:22 +03:00
melod1n 82695ccf6f Release 0.1.4 (#95)
update gh actions
2024-12-14 12:34:53 +03:00
336 changed files with 13530 additions and 4735 deletions
@@ -1,10 +1,10 @@
name: Android CI name: Android CI Build
on: on:
push: push:
branches: [ "master" ] branches: [ "master", "hotfix/*", "feature/*" ]
pull_request: pull_request:
branches: [ "master" ] branches: [ "master", "hotfix/*", "feature/*" ]
env: env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -12,33 +12,24 @@ env:
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }} RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
jobs: jobs:
build_apk_aab: build_apks:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
name: Build artifacts name: Build artifacts
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: set up JDK 17 - name: set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
cache: gradle cache: gradle
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Build and sign debug APKs - name: Build and sign release 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 APKs
run: ./gradlew assembleRelease run: ./gradlew assembleRelease
- name: Upload release APK - name: Upload release APK
@@ -46,3 +37,12 @@ jobs:
with: with:
name: app-release.apk name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk path: app/build/outputs/apk/release/app-release.apk
- 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
+46
View File
@@ -0,0 +1,46 @@
name: Android CI Release
on:
push:
branches: [ "release/*"]
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
jobs:
create_release:
runs-on: ubuntu-24.04
name: Build artifacts
steps:
- name: Checkout
uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build and sign release APK
run: ./gradlew assembleRelease
- name: Upload release APK
uses: actions/upload-artifact@v4
with:
name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk
- name: Build and sign release Bundle
run: ./gradlew bundleRelease
- name: Upload release Bundle
uses: actions/upload-artifact@v4
with:
name: app-release.aab
path: app/build/outputs/bundle/release/app-release.aab
+13 -10
View File
@@ -17,16 +17,19 @@ Unofficial messenger for russian social network VKontakte
- [ ] View archived conversations - [ ] View archived conversations
- [ ] Archive & unarchive conversations - [ ] Archive & unarchive conversations
- [x] Friends list - [x] Friends list
- [ ] Sort alphabetically, by priority or random - [x] Sort alphabetically, by priority or random
- [ ] Separate tab with only friends who are online - [x] Separate tab with only friends who are online
- [x] Settings screen - [x] Settings screen
- [ ] TODO - [ ] TODO
- [x] Chat screen - [x] Chat screen
- [ ] Pagination - [x] Pagination
- [x] Manual refresh - [x] Manual refresh
- [x] Message bubbles - [x] Message bubbles
- [x] Text - [x] Text
- [ ] Date - [x] Date
- [x] Read status
- [x] Edit status
- [x] Sending status
- [ ] Message's attachments - [ ] Message's attachments
- [ ] Photo - [ ] Photo
- [ ] Video - [ ] Video
@@ -35,19 +38,19 @@ Unofficial messenger for russian social network VKontakte
- [ ] Link - [ ] Link
- [ ] TODO - [ ] TODO
- [x] Send messages - [x] Send messages
- [ ] Pinned message - [x] Pinned message
- [ ] Pin & unpin messages - [x] Pin & unpin messages
- [ ] Reply to message - [ ] Reply to message
- [ ] Delete message - [x] Delete message
- [ ] Select multiple messages - [x] Select multiple messages
- [ ] Delete - [x] Delete
- [ ] Forward - [ ] Forward
- [ ] Forward in current chat - [ ] Forward in current chat
- [ ] Send attachments to chat - [ ] Send attachments to chat
- [ ] TODO - [ ] TODO
- [x] Chat materials (attachments) - [x] Chat materials (attachments)
- [x] Separate tabs for each attachment type - [x] Separate tabs for each attachment type
- [ ] Pagination - [x] Pagination
- [x] Manual refresh - [x] Manual refresh
- [x] View attachments - [x] View attachments
- [x] Open photo - [x] Open photo
+3 -2
View File
@@ -7,10 +7,10 @@ plugins {
} }
android { android {
namespace = "dev.meloda.fast" namespace = "dev.meloda.fastvk"
defaultConfig { defaultConfig {
applicationId = "dev.meloda.fast" applicationId = "dev.meloda.fastvk"
versionCode = libs.versions.versionCode.get().toInt() versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get() versionName = libs.versions.versionName.get()
@@ -77,6 +77,7 @@ dependencies {
implementation(projects.feature.friends) implementation(projects.feature.friends)
implementation(projects.feature.profile) implementation(projects.feature.profile)
implementation(projects.feature.photoviewer) implementation(projects.feature.photoviewer)
implementation(projects.feature.createchat)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.ui) implementation(projects.core.ui)
+3 -3
View File
@@ -22,7 +22,7 @@
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name=".presentation.MainActivity" android:name="dev.meloda.fast.presentation.MainActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
@@ -38,13 +38,13 @@
</activity> </activity>
<service <service
android:name=".service.longpolling.LongPollingService" android:name="dev.meloda.fast.service.longpolling.LongPollingService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service <service
android:name=".service.OnlineService" android:name="dev.meloda.fast.service.OnlineService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
@@ -24,6 +24,7 @@ import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.GetCurrentAccountUseCase import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.navigation.Main import dev.meloda.fast.navigation.Main
import dev.meloda.fast.settings.navigation.Settings import dev.meloda.fast.settings.navigation.Settings
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -36,14 +37,13 @@ interface MainViewModel {
val startDestination: StateFlow<Any?> val startDestination: StateFlow<Any?>
val isNeedToReplaceWithAuth: StateFlow<Boolean> val isNeedToReplaceWithAuth: StateFlow<Boolean>
val currentUser: StateFlow<VkUser?>
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean> val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean> val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
val isNeedToCheckNotificationsPermission: StateFlow<Boolean> val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
val isNeedToRequestNotifications: StateFlow<Boolean> val isNeedToRequestNotifications: StateFlow<Boolean>
val profileImageUrl: StateFlow<String?>
fun onError(error: BaseError) fun onError(error: BaseError)
fun onNavigatedToAuth() fun onNavigatedToAuth()
@@ -59,6 +59,8 @@ interface MainViewModel {
fun onNotificationsDeniedDialogDismissed() fun onNotificationsDeniedDialogDismissed()
fun onNotificationsRationaleDialogDismissed() fun onNotificationsRationaleDialogDismissed()
fun onNotificationsRationaleDialogCancelClicked() fun onNotificationsRationaleDialogCancelClicked()
fun onUserAuthenticated()
} }
class MainViewModelImpl( class MainViewModelImpl(
@@ -70,22 +72,24 @@ class MainViewModelImpl(
override val startDestination = MutableStateFlow<Any?>(null) override val startDestination = MutableStateFlow<Any?>(null)
override val isNeedToReplaceWithAuth = MutableStateFlow(false) override val isNeedToReplaceWithAuth = MutableStateFlow(false)
override val currentUser = MutableStateFlow<VkUser?>(null)
override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false) override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false) override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false) override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
override val isNeedToRequestNotifications = MutableStateFlow(false) override val isNeedToRequestNotifications = MutableStateFlow(false)
override val profileImageUrl = MutableStateFlow<String?>(null)
private var openNotificationsSettings = false private var openNotificationsSettings = false
private var openAppSettings = false private var openAppSettings = false
override fun onError(error: BaseError) { override fun onError(error: BaseError) {
when (error) { when (error) {
BaseError.SessionExpired -> { BaseError.SessionExpired,
BaseError.AccountBlocked -> {
isNeedToReplaceWithAuth.update { true } isNeedToReplaceWithAuth.update { true }
} }
else -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
} }
} }
@@ -168,17 +172,20 @@ class MainViewModelImpl(
disableBackgroundLongPoll() disableBackgroundLongPoll()
} }
override fun onUserAuthenticated() {
loadProfile()
}
private fun loadProfile() { private fun loadProfile() {
loadUserByIdUseCase(userId = null) loadUserByIdUseCase(userId = null)
.listenValue(viewModelScope) { state -> .listenValue(viewModelScope) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
profileImageUrl.emit(null) currentUser.emit(null)
}, },
success = { response -> success = { response ->
val user = response ?: return@listenValue val user = response ?: return@listenValue
currentUser.emit(user)
profileImageUrl.emit(user.photo100)
} }
) )
} }
@@ -209,7 +216,7 @@ class MainViewModelImpl(
} }
longPollController.setStateToApply( longPollController.setStateToApply(
if (AppSettings.Debug.longPollInBackground) { if (AppSettings.Experimental.longPollInBackground) {
LongPollState.Background LongPollState.Background
} else { } else {
LongPollState.InApp LongPollState.InApp
@@ -233,7 +240,7 @@ class MainViewModelImpl(
} }
private fun disableBackgroundLongPoll() { private fun disableBackgroundLongPoll() {
AppSettings.Debug.longPollInBackground = false AppSettings.Experimental.longPollInBackground = false
longPollController.setStateToApply(LongPollState.InApp) longPollController.setStateToApply(LongPollState.InApp)
} }
} }
@@ -16,6 +16,7 @@ import dev.meloda.fast.common.provider.Provider
import dev.meloda.fast.common.provider.ResourceProvider import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.common.provider.ResourceProviderImpl import dev.meloda.fast.common.provider.ResourceProviderImpl
import dev.meloda.fast.conversations.di.conversationsModule 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.domain.di.domainModule
import dev.meloda.fast.friends.di.friendsModule import dev.meloda.fast.friends.di.friendsModule
import dev.meloda.fast.languagepicker.di.languagePickerModule import dev.meloda.fast.languagepicker.di.languagePickerModule
@@ -26,8 +27,8 @@ import dev.meloda.fast.provider.ApiLanguageProvider
import dev.meloda.fast.service.longpolling.di.longPollModule import dev.meloda.fast.service.longpolling.di.longPollModule
import dev.meloda.fast.settings.di.settingsModule import dev.meloda.fast.settings.di.settingsModule
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.core.qualifier.qualifier import org.koin.core.qualifier.qualifier
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
@@ -46,10 +47,10 @@ val applicationModule = module {
longPollModule, longPollModule,
friendsModule, friendsModule,
profileModule, profileModule,
chatMaterialsModule chatMaterialsModule,
createChatModule
) )
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class // TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
singleOf(PreferenceManager::getDefaultSharedPreferences) singleOf(PreferenceManager::getDefaultSharedPreferences)
single<Resources> { androidContext().resources } single<Resources> { androidContext().resources }
@@ -61,7 +62,7 @@ val applicationModule = module {
qualifier = qualifier("main") qualifier = qualifier("main")
} }
single { single<ImageLoader> {
ImageLoader.Builder(get()) ImageLoader.Builder(get())
.crossfade(true) .crossfade(true)
.build() .build()
@@ -2,8 +2,7 @@ package dev.meloda.fast.navigation
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import dev.meloda.fast.MainViewModel import dev.meloda.fast.conversations.navigation.ConversationsGraph
import dev.meloda.fast.conversations.navigation.Conversations
import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.BottomNavigationItem import dev.meloda.fast.model.BottomNavigationItem
@@ -22,9 +21,10 @@ object Main
fun NavGraphBuilder.mainScreen( fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit, onSettingsButtonClicked: () -> Unit,
onConversationClicked: (conversationId: Int) -> Unit, onNavigateToMessagesHistory: (conversationId: Long) -> Unit,
onPhotoClicked: (url: String) -> Unit, onPhotoClicked: (url: String) -> Unit,
viewModel: MainViewModel onMessageClicked: (userid: Long) -> Unit,
onNavigateToCreateChat: () -> Unit
) { ) {
val navigationItems = ImmutableList.of( val navigationItems = ImmutableList.of(
BottomNavigationItem( BottomNavigationItem(
@@ -37,7 +37,7 @@ fun NavGraphBuilder.mainScreen(
titleResId = UiR.string.title_conversations, titleResId = UiR.string.title_conversations,
selectedIconResId = UiR.drawable.baseline_chat_24, selectedIconResId = UiR.drawable.baseline_chat_24,
unselectedIconResId = UiR.drawable.outline_chat_24, unselectedIconResId = UiR.drawable.outline_chat_24,
route = Conversations route = ConversationsGraph
), ),
BottomNavigationItem( BottomNavigationItem(
titleResId = UiR.string.title_profile, titleResId = UiR.string.title_profile,
@@ -52,9 +52,10 @@ fun NavGraphBuilder.mainScreen(
navigationItems = navigationItems, navigationItems = navigationItems,
onError = onError, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onConversationItemClicked = onConversationClicked, onNavigateToMessagesHistory = onNavigateToMessagesHistory,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
viewModel = viewModel onMessageClicked = onMessageClicked,
onNavigateToCreateChat = onNavigateToCreateChat
) )
} }
} }
@@ -24,7 +24,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -39,6 +38,7 @@ import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings 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.OnlineService
import dev.meloda.fast.service.longpolling.LongPollingService import dev.meloda.fast.service.longpolling.LongPollingService
import dev.meloda.fast.ui.model.DeviceSize import dev.meloda.fast.ui.model.DeviceSize
@@ -47,6 +47,7 @@ import dev.meloda.fast.ui.model.ThemeConfig
import dev.meloda.fast.ui.theme.AppTheme import dev.meloda.fast.ui.theme.AppTheme
import dev.meloda.fast.ui.theme.LocalSizeConfig import dev.meloda.fast.ui.theme.LocalSizeConfig
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.KoinContext import org.koin.compose.KoinContext
@@ -85,6 +86,7 @@ class MainActivity : AppCompatActivity() {
) )
createNotificationChannels() createNotificationChannels()
requestNotificationPermissions()
setContent { setContent {
KoinContext { KoinContext {
@@ -98,6 +100,8 @@ class MainActivity : AppCompatActivity() {
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>() val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
LifecycleResumeEffect(true) { LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent) viewModel.onAppResumed(intent)
onPauseOrDispose {} onPauseOrDispose {}
@@ -133,7 +137,8 @@ class MainActivity : AppCompatActivity() {
} }
} }
LaunchedEffect(longPollStateToApply) { LifecycleResumeEffect(longPollStateToApply) {
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
if (longPollStateToApply != LongPollState.Background) { if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched() if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply && longPollCurrentState != longPollStateToApply
@@ -147,6 +152,8 @@ class MainActivity : AppCompatActivity() {
inBackground = longPollStateToApply == LongPollState.Background inBackground = longPollStateToApply == LongPollState.Background
) )
} }
onPauseOrDispose {}
} }
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle() val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
@@ -202,6 +209,7 @@ class MainActivity : AppCompatActivity() {
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle() val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle() val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle() val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode) val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
@@ -214,7 +222,7 @@ class MainActivity : AppCompatActivity() {
setDarkMode, setDarkMode,
useSystemFont useSystemFont
) { ) {
mutableStateOf( derivedStateOf {
ThemeConfig( ThemeConfig(
darkMode = setDarkMode, darkMode = setDarkMode,
dynamicColors = dynamicColors, dynamicColors = dynamicColors,
@@ -222,14 +230,16 @@ class MainActivity : AppCompatActivity() {
amoledDark = amoledDark, amoledDark = amoledDark,
enableBlur = enableBlur, enableBlur = enableBlur,
enableMultiline = enableMultiline, enableMultiline = enableMultiline,
useSystemFont = useSystemFont useSystemFont = useSystemFont,
) enableAnimations = enableAnimations
) )
} }
}
CompositionLocalProvider( CompositionLocalProvider(
LocalThemeConfig provides themeConfig, LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig LocalSizeConfig provides sizeConfig,
LocalUser provides currentUser
) { ) {
AppTheme( AppTheme(
useDarkTheme = themeConfig.darkMode, useDarkTheme = themeConfig.darkMode,
@@ -250,12 +260,11 @@ class MainActivity : AppCompatActivity() {
val noCategoryName = getString(UiR.string.notification_channel_no_category_name) val noCategoryName = getString(UiR.string.notification_channel_no_category_name)
val noCategoryDescriptionText = val noCategoryDescriptionText =
getString(UiR.string.notification_channel_no_category_description) getString(UiR.string.notification_channel_no_category_description)
val noCategoryImportance = NotificationManagerCompat.IMPORTANCE_HIGH
val noCategoryChannel = val noCategoryChannel =
NotificationChannel( NotificationChannel(
AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED, AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED,
noCategoryName, noCategoryName,
noCategoryImportance NotificationManager.IMPORTANCE_HIGH
).apply { ).apply {
description = noCategoryDescriptionText description = noCategoryDescriptionText
} }
@@ -263,12 +272,11 @@ class MainActivity : AppCompatActivity() {
val longPollName = getString(UiR.string.notification_channel_long_polling_service_name) val longPollName = getString(UiR.string.notification_channel_long_polling_service_name)
val longPollDescriptionText = val longPollDescriptionText =
getString(UiR.string.notification_channel_long_polling_service_description) getString(UiR.string.notification_channel_long_polling_service_description)
val longPollImportance = NotificationManagerCompat.IMPORTANCE_NONE
val longPollChannel = val longPollChannel =
NotificationChannel( NotificationChannel(
AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING, AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING,
longPollName, longPollName,
longPollImportance NotificationManager.IMPORTANCE_NONE
).apply { ).apply {
description = longPollDescriptionText description = longPollDescriptionText
} }
@@ -285,12 +293,28 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun requestNotificationPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
REQUEST_NOTIFICATION_PERMISSION_CODE
)
}
}
private val longPollingServiceIntent by lazy {
Intent(this, LongPollingService::class.java)
}
private val onlineServiceIntent by lazy {
Intent(this, OnlineService::class.java)
}
private fun toggleLongPollService( private fun toggleLongPollService(
enable: Boolean, enable: Boolean,
inBackground: Boolean = AppSettings.Debug.longPollInBackground inBackground: Boolean = AppSettings.Experimental.longPollInBackground
) { ) {
if (enable) { if (enable) {
val longPollIntent = Intent(this, LongPollingService::class.java) val longPollIntent = longPollingServiceIntent
if (inBackground) { if (inBackground) {
ContextCompat.startForegroundService(this, longPollIntent) ContextCompat.startForegroundService(this, longPollIntent)
@@ -298,22 +322,22 @@ class MainActivity : AppCompatActivity() {
startService(longPollIntent) startService(longPollIntent)
} }
} else { } else {
stopService(Intent(this, LongPollingService::class.java)) stopService(longPollingServiceIntent)
} }
} }
private fun toggleOnlineService(enable: Boolean) { private fun toggleOnlineService(enable: Boolean) {
if (enable) { if (enable) {
startService(Intent(this, OnlineService::class.java)) startService(onlineServiceIntent)
} else { } else {
stopService(Intent(this, OnlineService::class.java)) stopService(onlineServiceIntent)
} }
} }
private fun stopServices() { private fun stopServices() {
toggleOnlineService(enable = false) toggleOnlineService(enable = false)
val asForeground = AppSettings.Debug.longPollInBackground val asForeground = AppSettings.Experimental.longPollInBackground
if (!asForeground) { if (!asForeground) {
toggleLongPollService(enable = false) toggleLongPollService(enable = false)
@@ -324,4 +348,8 @@ class MainActivity : AppCompatActivity() {
super.onDestroy() super.onDestroy()
stopServices() stopServices()
} }
companion object {
private const val REQUEST_NOTIFICATION_PERMISSION_CODE = 1
}
} }
@@ -1,12 +1,12 @@
package dev.meloda.fast.presentation package dev.meloda.fast.presentation
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -16,6 +16,7 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -25,19 +26,21 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.navigation import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import coil.compose.SubcomposeAsyncImage import coil.compose.SubcomposeAsyncImage
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.MainViewModel import dev.meloda.fast.conversations.navigation.Conversations
import dev.meloda.fast.conversations.navigation.conversationsScreen import dev.meloda.fast.conversations.navigation.ConversationsGraph
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.friends.navigation.friendsScreen
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.BottomNavigationItem import dev.meloda.fast.model.BottomNavigationItem
@@ -45,7 +48,10 @@ import dev.meloda.fast.navigation.MainGraph
import dev.meloda.fast.profile.navigation.profileScreen import dev.meloda.fast.profile.navigation.profileScreen
import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalNavController
import dev.meloda.fast.ui.theme.LocalReselectedTab
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
@OptIn(ExperimentalHazeMaterialsApi::class) @OptIn(ExperimentalHazeMaterialsApi::class)
@@ -54,36 +60,59 @@ fun MainScreen(
navigationItems: ImmutableList<BottomNavigationItem>, navigationItems: ImmutableList<BottomNavigationItem>,
onError: (BaseError) -> Unit = {}, onError: (BaseError) -> Unit = {},
onSettingsButtonClicked: () -> Unit = {}, onSettingsButtonClicked: () -> Unit = {},
onConversationItemClicked: (conversationId: Int) -> Unit = {}, onNavigateToMessagesHistory: (conversationId: Long) -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {},
viewModel: MainViewModel onMessageClicked: (userid: Long) -> Unit = {},
onNavigateToCreateChat: () -> Unit = {}
) { ) {
val currentTheme = LocalThemeConfig.current val theme = LocalThemeConfig.current
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
val navController = rememberNavController() val navController = rememberNavController()
val profileImageUrl by viewModel.profileImageUrl.collectAsStateWithLifecycle()
var selectedItemIndex by rememberSaveable { var selectedItemIndex by rememberSaveable {
mutableIntStateOf(1) mutableIntStateOf(1)
} }
BackHandler(enabled = selectedItemIndex != 1) {
val index = 1
val currentRoute = navigationItems[selectedItemIndex].route
selectedItemIndex = 1
navController.navigate(navigationItems[index].route) {
popUpTo(route = currentRoute) {
inclusive = true
}
}
}
val user = LocalUser.current
val profileImageUrl by remember(user) {
derivedStateOf { user?.photo100 }
}
var tabReselected by remember {
mutableStateOf(
navigationItems.associate {
it.route to false
}
)
}
Scaffold( Scaffold(
bottomBar = { bottomBar = {
NavigationBar( NavigationBar(
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.then( .then(
if (currentTheme.enableBlur) { if (theme.enableBlur) {
Modifier.hazeChild( Modifier.hazeEffect(
state = hazeState, state = hazeState,
style = HazeMaterials.thick() style = HazeMaterials.thick()
) )
} else Modifier } else Modifier
) ),
.fillMaxWidth(), containerColor = if (theme.enableBlur) Color.Transparent
containerColor = NavigationBarDefaults.containerColor.copy( else NavigationBarDefaults.containerColor
alpha = if (currentTheme.enableBlur) 0f else 1f
)
) { ) {
navigationItems.forEachIndexed { index, item -> navigationItems.forEachIndexed { index, item ->
NavigationBarItem( NavigationBarItem(
@@ -98,6 +127,10 @@ fun MainScreen(
inclusive = true inclusive = true
} }
} }
} else {
tabReselected = tabReselected.toMutableMap().also {
it[navigationItems[index].route] = true
}
} }
}, },
icon = { icon = {
@@ -121,9 +154,7 @@ fun MainScreen(
.size(24.dp) .size(24.dp)
.clip(CircleShape) .clip(CircleShape)
.alpha(if (isLoading) 0f else 1f), .alpha(if (isLoading) 0f else 1f),
onSuccess = { onSuccess = { isLoading = false }
isLoading = false
}
) )
} else { } else {
Icon( Icon(
@@ -144,11 +175,12 @@ fun MainScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(bottom = if (currentTheme.enableBlur) 0.dp else padding.calculateBottomPadding())
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalHazeState provides hazeState, LocalHazeState provides hazeState,
LocalBottomPadding provides if (currentTheme.enableBlur) padding.calculateBottomPadding() else 0.dp LocalBottomPadding provides padding.calculateBottomPadding(),
LocalReselectedTab provides tabReselected,
LocalNavController provides navController
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
@@ -156,23 +188,35 @@ fun MainScreen(
enterTransition = { fadeIn(animationSpec = tween(200)) }, enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) } exitTransition = { fadeOut(animationSpec = tween(200)) }
) { ) {
navigation<MainGraph>(startDestination = navigationItems[selectedItemIndex].route) { navigation<MainGraph>(
startDestination = navigationItems[selectedItemIndex].route,
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
friendsScreen( friendsScreen(
onError = onError, onError = onError,
navController = navController, onPhotoClicked = onPhotoClicked,
onPhotoClicked = onPhotoClicked onMessageClicked = onMessageClicked,
onScrolledToTop = {
tabReselected = tabReselected.toMutableMap().also {
it[Friends] = false
}
},
) )
conversationsScreen( conversationsGraph(
onError = onError, onError = onError,
onConversationItemClicked = onConversationItemClicked, onNavigateToMessagesHistory = onNavigateToMessagesHistory,
navController = navController, onNavigateToCreateChat = onNavigateToCreateChat,
onPhotoClicked = onPhotoClicked onScrolledToTop = {
tabReselected = tabReselected.toMutableMap().also {
it[ConversationsGraph] = false
}
}
) )
profileScreen( profileScreen(
onError = onError, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked
navController = navController
) )
} }
} }
@@ -10,6 +10,7 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -25,6 +26,8 @@ import dev.meloda.fast.auth.authNavGraph
import dev.meloda.fast.auth.navigateToAuth import dev.meloda.fast.auth.navigateToAuth
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
import dev.meloda.fast.conversations.navigation.createChatScreen
import dev.meloda.fast.conversations.navigation.navigateToCreateChat
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
@@ -36,6 +39,8 @@ import dev.meloda.fast.photoviewer.navigation.photoViewScreen
import dev.meloda.fast.settings.navigation.navigateToSettings import dev.meloda.fast.settings.navigation.navigateToSettings
import dev.meloda.fast.settings.navigation.settingsScreen import dev.meloda.fast.settings.navigation.settingsScreen
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.theme.LocalNavController
import dev.meloda.fast.ui.theme.LocalNavRootController
@Composable @Composable
fun RootScreen( fun RootScreen(
@@ -109,6 +114,10 @@ fun RootScreen(
} }
if (startDestination != null) { if (startDestination != null) {
CompositionLocalProvider(
LocalNavRootController provides navController,
LocalNavController provides navController
) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = requireNotNull(startDestination), startDestination = requireNotNull(startDestination),
@@ -116,26 +125,38 @@ fun RootScreen(
exitTransition = { fadeOut(animationSpec = tween(200)) } exitTransition = { fadeOut(animationSpec = tween(200)) }
) { ) {
authNavGraph( authNavGraph(
onNavigateToMain = navController::navigateToMain, onNavigateToMain = {
viewModel.onUserAuthenticated()
navController.navigateToMain()
},
navController = navController navController = navController
) )
mainScreen( mainScreen(
onError = viewModel::onError, onError = viewModel::onError,
onSettingsButtonClicked = navController::navigateToSettings, onSettingsButtonClicked = navController::navigateToSettings,
onConversationClicked = navController::navigateToMessagesHistory, onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }, onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
viewModel = viewModel onMessageClicked = navController::navigateToMessagesHistory,
onNavigateToCreateChat = navController::navigateToCreateChat
) )
messagesHistoryScreen( messagesHistoryScreen(
onError = viewModel::onError, onError = viewModel::onError,
onBack = navController::navigateUp, onBack = navController::navigateUp,
onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials onNavigateToChatMaterials = navController::navigateToChatMaterials
) )
chatMaterialsScreen( chatMaterialsScreen(
onBack = navController::navigateUp, onBack = navController::navigateUp,
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) } onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
) )
createChatScreen(
onChatCreated = { conversationId ->
navController.popBackStack()
navController.navigateToMessagesHistory(conversationId)
},
navController = navController
)
settingsScreen( settingsScreen(
onBack = navController::navigateUp, onBack = navController::navigateUp,
@@ -147,6 +168,7 @@ fun RootScreen(
photoViewScreen(onBack = navController::navigateUp) photoViewScreen(onBack = navController::navigateUp)
} }
} }
}
} }
fun NavController.navigateToMain() { fun NavController.navigateToMain() {
@@ -95,11 +95,6 @@ class OnlineService : Service() {
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } } }.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
} }
override fun onLowMemory() {
Log.d(STATE_TAG, "onLowMemory")
super.onLowMemory()
}
override fun onDestroy() { override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy") Log.d(STATE_TAG, "onDestroy")
@@ -16,11 +16,11 @@ import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.LongPollUseCase
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.LongPollUseCase
import dev.meloda.fast.model.api.data.LongPollUpdates import dev.meloda.fast.model.api.data.LongPollUpdates
import dev.meloda.fast.model.api.data.VkLongPollData import dev.meloda.fast.model.api.data.VkLongPollData
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
@@ -30,11 +30,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.time.Duration.Companion.seconds
class LongPollingService : Service() { class LongPollingService : Service() {
@@ -42,15 +44,9 @@ class LongPollingService : Service() {
private val job = SupervisorJob() private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> private val exceptionHandler =
Log.e(TAG, "error: $throwable") CoroutineExceptionHandler { _, throwable ->
handleError(throwable)
if (throwable !is NoAccessTokenException) {
throwable.printStackTrace()
}
longPollController.updateCurrentState(LongPollState.Exception)
longPollController.setStateToApply(LongPollState.Exception)
} }
private val coroutineContext: CoroutineContext private val coroutineContext: CoroutineContext
@@ -63,6 +59,8 @@ class LongPollingService : Service() {
private var currentJob: Job? = null private var currentJob: Job? = null
private val inBackground get() = AppSettings.Experimental.longPollInBackground
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(STATE_TAG, "onCreate()") Log.d(STATE_TAG, "onCreate()")
@@ -76,21 +74,12 @@ class LongPollingService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (startId > 1) return START_STICKY if (startId > 1) return START_STICKY
val inBackground = AppSettings.Debug.longPollInBackground
Log.d( Log.d(
STATE_TAG, STATE_TAG,
"onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this" "onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this"
) )
if (currentJob != null) { startJob()
currentJob?.cancel()
currentJob = null
}
coroutineScope.launch {
currentJob = startPolling().also { it.join() }
}
val openCategorySettingsIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val openCategorySettingsIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
@@ -108,11 +97,6 @@ class LongPollingService : Service() {
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
) )
longPollController.updateCurrentState(
if (inBackground) LongPollState.Background
else LongPollState.InApp
)
if (inBackground) { if (inBackground) {
val notification = val notification =
NotificationsUtils.createNotification( NotificationsUtils.createNotification(
@@ -134,17 +118,33 @@ class LongPollingService : Service() {
return START_STICKY return START_STICKY
} }
private fun startJob() {
if (currentJob != null) {
currentJob?.cancel()
currentJob = null
}
coroutineScope.launch {
currentJob = startPolling().also { it.join() }
}
}
private fun startPolling(): Job { private fun startPolling(): Job {
if (job.isCompleted || job.isCancelled) { if (job.isCompleted || job.isCancelled) {
Log.d(STATE_TAG, "job is completed or cancelled") Log.d(STATE_TAG, "Job is completed or cancelled")
throw Exception("Job is over") throw Exception("Job is over")
} }
Log.d(STATE_TAG, "job started") Log.d(STATE_TAG, "Starting job...")
return coroutineScope.launch(coroutineContext) {
longPollController.updateCurrentState(
if (inBackground) LongPollState.Background
else LongPollState.InApp
)
return coroutineScope.launch {
if (UserConfig.accessToken.isEmpty()) { if (UserConfig.accessToken.isEmpty()) {
throw NoAccessTokenException throw NoAccessTokenException()
} }
var serverInfo = getServerInfo() var serverInfo = getServerInfo()
@@ -246,6 +246,21 @@ class LongPollingService : Service() {
} }
} }
private fun handleError(throwable: Throwable) {
Log.e(TAG, "error: $throwable")
if (throwable !is NoAccessTokenException) {
throwable.printStackTrace()
}
coroutineScope.launch {
delay(5.seconds)
startJob()
}
longPollController.updateCurrentState(LongPollState.Exception)
}
override fun onDestroy() { override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy") Log.d(STATE_TAG, "onDestroy")
longPollController.updateCurrentState(LongPollState.Stopped) longPollController.updateCurrentState(LongPollState.Stopped)
@@ -258,10 +273,9 @@ class LongPollingService : Service() {
super.onDestroy() super.onDestroy()
} }
override fun onLowMemory() { override fun onTrimMemory(level: Int) {
Log.d(STATE_TAG, "onLowMemory") Log.d(STATE_TAG, "onTrimMemory. Level: $level")
longPollController.updateCurrentState(LongPollState.Stopped) super.onTrimMemory(level)
super.onLowMemory()
} }
companion object { companion object {
@@ -276,4 +290,4 @@ class LongPollingService : Service() {
} }
private data class LongPollException(override val message: String) : Throwable() private data class LongPollException(override val message: String) : Throwable()
private data object NoAccessTokenException : Throwable() private class NoAccessTokenException : Throwable()
+3 -3
View File
@@ -7,13 +7,13 @@ plugins {
group = "dev.meloda.fast.buildlogic" group = "dev.meloda.fast.buildlogic"
java { java {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_21
} }
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget = JvmTarget.JVM_17 jvmTarget = JvmTarget.JVM_21
} }
} }
@@ -9,8 +9,8 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.provideDelegate
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension
internal fun Project.configureKotlinAndroid( internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>, commonExtension: CommonExtension<*, *, *, *, *, *>,
@@ -23,8 +23,8 @@ internal fun Project.configureKotlinAndroid(
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_21
} }
} }
@@ -33,14 +33,14 @@ internal fun Project.configureKotlinAndroid(
internal fun Project.configureKotlinJvm() { internal fun Project.configureKotlinJvm() {
extensions.configure<JavaPluginExtension> { extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_21
} }
configureKotlin<KotlinJvmProjectExtension>() configureKotlin<KotlinJvmProjectExtension>()
} }
private inline fun <reified T : KotlinTopLevelExtension> Project.configureKotlin() = configure<T> { private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() = configure<T> {
// Treat all Kotlin warnings as errors (disabled by default) // Treat all Kotlin warnings as errors (disabled by default)
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
val warningsAsErrors: String? by project val warningsAsErrors: String? by project
@@ -49,7 +49,7 @@ private inline fun <reified T : KotlinTopLevelExtension> Project.configureKotlin
is KotlinJvmProjectExtension -> compilerOptions is KotlinJvmProjectExtension -> compilerOptions
else -> TODO("Unsupported project extension $this ${T::class}") else -> TODO("Unsupported project extension $this ${T::class}")
}.apply { }.apply {
jvmTarget = JvmTarget.JVM_17 jvmTarget = JvmTarget.JVM_21
allWarningsAsErrors = warningsAsErrors.toBoolean() allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs.addAll( freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn", "-opt-in=kotlin.RequiresOptIn",
@@ -4,7 +4,7 @@ object AppConstants {
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive" const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
const val API_VERSION = "5.173" const val API_VERSION = "5.238"
const val URL_OAUTH = "https://oauth.vk.com" const val URL_OAUTH = "https://oauth.vk.com"
const val URL_API = "https://api.vk.com/method" const val URL_API = "https://api.vk.com/method"
@@ -5,12 +5,12 @@ object VkConstants {
const val GROUP_FIELDS = "description,members_count,counters,status,verified" const val GROUP_FIELDS = "description,members_count,counters,status,verified"
const val USER_FIELDS = const val USER_FIELDS =
"photo_50,photo_100,photo_200,photo_400_orig,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info,bdate" "photo_50,photo_100,photo_200,photo_400_orig,status,screen_name,online_info,last_seen,verified,sex,bdate"
const val ALL_FIELDS = const val ALL_FIELDS =
"$USER_FIELDS,$GROUP_FIELDS" "$USER_FIELDS,$GROUP_FIELDS"
const val LP_VERSION = 10 const val LP_VERSION = 19
const val VK_APP_ID = "2274003" const val VK_APP_ID = "2274003"
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
@@ -18,6 +18,11 @@ object VkConstants {
const val FAST_GROUP_ID = -119516304 const val FAST_GROUP_ID = -119516304
const val FAST_APP_ID = "6964679" const val FAST_APP_ID = "6964679"
const val MESSENGER_APP_ID = 51453752
const val MESSENGER_APP_SECRET = "4UyuCUsdK8pVCNoeQuGi"
const val MESSENGER_APP_SCOPE = 1454174
object Auth { object Auth {
const val SCOPE = "notify," + const val SCOPE = "notify," +
"friends," + "friends," +
@@ -1,7 +1,5 @@
package dev.meloda.fast.common.extensions package dev.meloda.fast.common.extensions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -25,6 +23,20 @@ fun <T> MutableList<T>.addIf(element: T, condition: () -> Boolean) {
if (condition.invoke()) add(element) 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( fun <T> Flow<T>.listenValue(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
action: suspend (T) -> Unit action: suspend (T) -> Unit
@@ -77,6 +89,11 @@ fun <T> MutableStateFlow<T>.setValue(function: (T) -> T) {
update { newValue } update { newValue }
} }
fun <T> MutableStateFlow<T>.updateValue(block: T.() -> T) {
val newValue = block(value)
update { newValue }
}
fun Any.asInt(): Int { fun Any.asInt(): Int {
return when (this) { return when (this) {
is Number -> this.toInt() is Number -> this.toInt()
@@ -85,6 +102,14 @@ fun Any.asInt(): Int {
} }
} }
fun Any.asLong(): Long {
return when(this) {
is Number -> this.toLong()
else -> throw IllegalArgumentException("Object is not numeric")
}
}
fun <T> Any.toList(mapper: (old: Any) -> T): List<T> { fun <T> Any.toList(mapper: (old: Any) -> T): List<T> {
return when (this) { return when (this) {
is List<*> -> this.mapNotNull { it?.run(mapper) } is List<*> -> this.mapNotNull { it?.run(mapper) }
@@ -86,7 +86,7 @@ object AndroidUtils {
action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Settings.ACTION_SECURITY_SETTINGS Settings.ACTION_SECURITY_SETTINGS
} else { } else {
data = Uri.parse("package:dev.meloda.fast") data = Uri.parse("package:dev.meloda.fastvk")
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
} }
}) })
@@ -20,7 +20,7 @@ sealed class State<out T> {
data object ConnectionError : Error() data object ConnectionError : Error()
data object Unknown : Error() data object UnknownError : Error()
data object InternalError : Error() data object InternalError : Error()
@@ -31,21 +31,21 @@ sealed class State<out T> {
companion object { companion object {
val UNKNOWN_ERROR = Error.Unknown val UNKNOWN_ERROR = Error.UnknownError
} }
} }
inline fun <T> State<T>.processState( inline fun <T> State<T>.processState(
error: (error: State.Error) -> (Unit), error: (error: State.Error) -> Unit,
success: (data: T) -> (Unit), success: (data: T) -> Unit,
idle: (() -> (Unit)) = {}, idle: (() -> (Unit)) = {},
loading: (() -> (Unit)) = {}, loading: (() -> (Unit)) = {},
any: () -> Unit = {} any: () -> Unit = {}
) { ) {
when (this) { when (this) {
is State.Error -> { is State.Error -> {
error(this)
any() any()
error(this)
} }
State.Idle -> idle() State.Idle -> idle()
@@ -53,17 +53,47 @@ inline fun <T> State<T>.processState(
State.Loading -> loading() State.Loading -> loading()
is State.Success -> { is State.Success -> {
success(data)
any() any()
success(data)
} }
} }
} }
fun OAuthErrorDomain?.toStateApiError(): State.Error {
if (this == null) return State.Error.ConnectionError
return State.Error.OAuthError(this)
}
fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) { fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) {
null -> State.Error.ConnectionError null -> State.Error.ConnectionError
else -> State.Error.ApiError(VkErrorCode.parse(code), message) else -> State.Error.ApiError(VkErrorCode.parse(code), message)
} }
fun <T : Any> ApiResult<T, OAuthErrorDomain>.asState() = when (this) {
is ApiResult.Success -> State.Success(this.value)
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
}
fun <T : Any, N> ApiResult<T, OAuthErrorDomain>.asState(successMapper: (T) -> N) =
when (this) {
is ApiResult.Success -> State.Success(successMapper(this.value))
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
}
fun <T : Any, E : Any> ApiResult<T, E>.success(): T =
when (this) {
is ApiResult.Success -> value
else -> throw IllegalArgumentException()
}
fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) { fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
is ApiResult.Success -> State.Success(this.value) is ApiResult.Success -> State.Success(this.value)
@@ -73,11 +103,12 @@ fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
} }
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) = when (this) { fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) =
when (this) {
is ApiResult.Success -> State.Success(successMapper(this.value)) is ApiResult.Success -> State.Success(successMapper(this.value))
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError() is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
} }
@@ -6,17 +6,18 @@ object UserConfig {
private const val ARG_CURRENT_USER_ID = "current_user_id" private const val ARG_CURRENT_USER_ID = "current_user_id"
var currentUserId: Int = -1 var currentUserId: Long = -1
get() = AppSettings.getInt(ARG_CURRENT_USER_ID, -1) get() = AppSettings.getLong(ARG_CURRENT_USER_ID, -1)
set(value) { set(value) {
field = value field = value
AppSettings.edit { putInt(ARG_CURRENT_USER_ID, value) } AppSettings.edit { putLong(ARG_CURRENT_USER_ID, value) }
} }
var userId: Int = -1 var userId: Long = -1
var accessToken: String = "" var accessToken: String = ""
var fastToken: String? = "" var fastToken: String? = ""
var trustedHash: String? = null var trustedHash: String? = null
var exchangeToken: String? = null
fun clear() { fun clear() {
currentUserId = -1 currentUserId = -1
@@ -10,7 +10,7 @@ class VkGroupsMap(
private val groups: List<VkGroupDomain> private val groups: List<VkGroupDomain>
) { ) {
private val map: HashMap<Int, VkGroupDomain> by lazy { private val map: HashMap<Long, VkGroupDomain> by lazy {
HashMap(groups.associateBy(VkGroupDomain::id)) HashMap(groups.associateBy(VkGroupDomain::id))
} }
@@ -36,7 +36,7 @@ class VkGroupsMap(
if (message.fromId >= 0) null if (message.fromId >= 0) null
else map[abs(message.fromId)] else map[abs(message.fromId)]
fun group(groupId: Int): VkGroupDomain? = map[abs(groupId)] fun group(groupId: Long): VkGroupDomain? = map[abs(groupId)]
companion object { companion object {
@@ -1,5 +1,6 @@
package dev.meloda.fast.data 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.VkContactDomain
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkGroupDomain
@@ -9,11 +10,11 @@ import kotlin.math.abs
object VkMemoryCache { object VkMemoryCache {
private val users: HashMap<Int, VkUser> = hashMapOf() private val users: HashMap<Long, VkUser> = hashMapOf()
private val groups: HashMap<Int, VkGroupDomain> = hashMapOf() private val groups: HashMap<Long, VkGroupDomain> = hashMapOf()
private val messages: HashMap<Int, VkMessage> = hashMapOf() private val messages: HashMap<Long, VkMessage> = hashMapOf()
private val conversations: HashMap<Int, VkConversation> = hashMapOf() private val conversations: HashMap<Long, VkConversation> = hashMapOf()
private val contacts: HashMap<Int, VkContactDomain> = hashMapOf() private val contacts: HashMap<Long, VkContactDomain> = hashMapOf()
fun appendUsers(users: List<VkUser>) { fun appendUsers(users: List<VkUser>) {
users.forEach { user -> VkMemoryCache.users[user.id] = user } users.forEach { user -> VkMemoryCache.users[user.id] = user }
@@ -37,83 +38,83 @@ object VkMemoryCache {
contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact } contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact }
} }
operator fun set(userId: Int, user: VkUser) { operator fun set(userid: Long, user: VkUser) {
users[userId] = user users[userId] = user
} }
operator fun set(groupId: Int, group: VkGroupDomain) { operator fun set(groupId: Long, group: VkGroupDomain) {
groups[groupId] = group groups[groupId] = group
} }
operator fun set(messageId: Int, message: VkMessage) { operator fun set(messageId: Long, message: VkMessage) {
messages[messageId] = message messages[messageId] = message
} }
operator fun set(conversationId: Int, conversation: VkConversation) { operator fun set(conversationId: Long, conversation: VkConversation) {
conversations[conversationId] = conversation conversations[conversationId] = conversation
} }
operator fun set(contactId: Int, contact: VkContactDomain) { operator fun set(contactId: Long, contact: VkContactDomain) {
contacts[contactId] = contact contacts[contactId] = contact
} }
fun getUser(id: Int): VkUser? { fun getUser(id: Long): VkUser? {
return getUsers(id).firstOrNull() return getUsers(id).firstOrNull()
} }
fun getUsers(vararg ids: Int): List<VkUser> { fun getUsers(vararg ids: Long): List<VkUser> {
return getUsers(ids.toList()) return getUsers(ids.toList())
} }
fun getUsers(ids: List<Int>): List<VkUser> { fun getUsers(ids: List<Long>): List<VkUser> {
return ids.mapNotNull { id -> users[id] } return ids.mapNotNull { id -> users[id] }
} }
fun getGroup(id: Int): VkGroupDomain? { fun getGroup(id: Long): VkGroupDomain? {
return getGroups(id).firstOrNull() return getGroups(id).firstOrNull()
} }
fun getGroups(vararg ids: Int): List<VkGroupDomain> { fun getGroups(vararg ids: Long): List<VkGroupDomain> {
return getGroups(ids.toList()) return getGroups(ids.toList())
} }
fun getGroups(ids: List<Int>): List<VkGroupDomain> { fun getGroups(ids: List<Long>): List<VkGroupDomain> {
return ids.mapNotNull { id -> groups[id] } return ids.mapNotNull { id -> groups[id] }
} }
fun getMessage(id: Int): VkMessage? { fun getMessage(id: Long): VkMessage? {
return getMessages(id).firstOrNull() return getMessages(id).firstOrNull()
} }
fun getMessages(vararg ids: Int): List<VkMessage> { fun getMessages(vararg ids: Long): List<VkMessage> {
return getMessages(ids.toList()) return getMessages(ids.toList())
} }
fun getMessages(ids: List<Int>): List<VkMessage> { fun getMessages(ids: List<Long>): List<VkMessage> {
return ids.mapNotNull { id -> messages[id] } return ids.mapNotNull { id -> messages[id] }
} }
fun getConversation(id: Int): VkConversation? { fun getConversation(id: Long): VkConversation? {
return getConversations(id).firstOrNull() return getConversations(id).firstOrNull()
} }
fun getConversations(vararg ids: Int): List<VkConversation> { fun getConversations(vararg ids: Long): List<VkConversation> {
return getConversations(ids.toList()) return getConversations(ids.toList())
} }
fun getConversations(ids: List<Int>): List<VkConversation> { fun getConversations(ids: List<Long>): List<VkConversation> {
return ids.mapNotNull { id -> conversations[id] } return ids.mapNotNull { id -> conversations[id] }
} }
fun getContact(id: Int): VkContactDomain? { fun getContact(id: Long): VkContactDomain? {
return getContacts(id).firstOrNull() return getContacts(id).firstOrNull()
} }
fun getContacts(vararg ids: Int): List<VkContactDomain> { fun getContacts(vararg ids: Long): List<VkContactDomain> {
return getContacts(ids.toList()) return getContacts(ids.toList())
} }
fun getContacts(ids: List<Int>): List<VkContactDomain> { fun getContacts(ids: List<Long>): List<VkContactDomain> {
return ids.mapNotNull { id -> contacts[id] } return ids.mapNotNull { id -> contacts[id] }
} }
} }
@@ -1,5 +1,6 @@
package dev.meloda.fast.data 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.data.VkMessageData
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
@@ -9,7 +10,7 @@ class VkUsersMap(
private val users: List<VkUser> private val users: List<VkUser>
) { ) {
private val map: HashMap<Int, VkUser> by lazy { private val map: HashMap<Long, VkUser> by lazy {
HashMap(users.associateBy(VkUser::id)) HashMap(users.associateBy(VkUser::id))
} }
@@ -35,7 +36,7 @@ class VkUsersMap(
if (message.fromId > 0) map[message.fromId] if (message.fromId > 0) map[message.fromId]
else null else null
fun user(userId: Int): VkUser? = map[userId] fun user(userid: Long): VkUser? = map[userId]
companion object { companion object {
@@ -0,0 +1,34 @@
package dev.meloda.fast.data
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.network.VkErrorCode
object VkUtils {
fun parseError(error: State.Error): BaseError? {
return when (error) {
is State.Error.ApiError -> {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
if (error.errorMessage.startsWith(
"User authorization failed: user is blocked."
)
) {
BaseError.AccountBlocked
} else {
BaseError.SessionExpired
}
}
else -> BaseError.SimpleError(message = error.errorMessage)
}
}
State.Error.ConnectionError -> BaseError.ConnectionError
State.Error.InternalError -> BaseError.InternalError
State.Error.UnknownError -> BaseError.UnknownError
else -> null
}
}
}
@@ -1,12 +1,32 @@
package dev.meloda.fast.data.api.auth package dev.meloda.fast.data.api.auth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import com.slack.eithernet.ApiResult
interface AuthRepository { interface AuthRepository {
suspend fun logout(): ApiResult<Int, RestApiErrorDomain>
suspend fun validatePhone( suspend fun validatePhone(
validationSid: String validationSid: String
): ApiResult<ValidatePhoneResponse, RestApiErrorDomain> ): ApiResult<ValidatePhoneResponse, RestApiErrorDomain>
suspend fun getAnonymToken(
clientId: String,
clientSecret: String
): ApiResult<GetAnonymTokenResponse, RestApiErrorDomain>
suspend fun exchangeSilentToken(
anonymToken: String,
silentToken: String,
silentUuid: String
): ApiResult<ExchangeSilentTokenResponse, RestApiErrorDomain>
suspend fun getExchangeToken(
accessToken: String
): ApiResult<GetExchangeTokenResponse, RestApiErrorDomain>
} }
@@ -1,10 +1,17 @@
package dev.meloda.fast.data.api.auth package dev.meloda.fast.data.api.auth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.model.api.requests.ExchangeSilentTokenRequest
import dev.meloda.fast.model.api.requests.GetAnonymTokenRequest
import dev.meloda.fast.model.api.requests.GetExchangeTokenRequest
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.service.auth.AuthService import dev.meloda.fast.network.service.auth.AuthService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -12,9 +19,50 @@ class AuthRepositoryImpl(
private val service: AuthService private val service: AuthService
) : AuthRepository { ) : AuthRepository {
override suspend fun logout(): ApiResult<Int, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
service.logout(
clientId = VkConstants.MESSENGER_APP_ID.toString(),
clientSecret = VkConstants.MESSENGER_APP_SECRET
).mapApiDefault()
}
override suspend fun validatePhone( override suspend fun validatePhone(
validationSid: String validationSid: String
): ApiResult<ValidatePhoneResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<ValidatePhoneResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
service.validatePhone(validationSid).mapApiDefault() service.validatePhone(validationSid).mapApiDefault()
} }
override suspend fun getAnonymToken(
clientId: String,
clientSecret: String
): ApiResult<GetAnonymTokenResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = GetAnonymTokenRequest(
clientId = clientId,
clientSecret = clientSecret
)
service.getAnonymToken(requestModel.map).mapApiDefault()
}
override suspend fun exchangeSilentToken(
anonymToken: String,
silentToken: String,
silentUuid: String
): ApiResult<ExchangeSilentTokenResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ExchangeSilentTokenRequest(
anonymToken = anonymToken,
silentToken = silentToken,
silentUuid = silentUuid
)
service.exchangeSilentToken(requestModel.map).mapApiDefault()
}
override suspend fun getExchangeToken(
accessToken: String
): ApiResult<GetExchangeTokenResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = GetExchangeTokenRequest(accessToken = accessToken)
service.getExchangeToken(requestModel.map).mapApiDefault()
}
} }
@@ -1,18 +1,30 @@
package dev.meloda.fast.data.api.conversations package dev.meloda.fast.data.api.conversations
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import com.slack.eithernet.ApiResult
interface ConversationsRepository { interface ConversationsRepository {
suspend fun storeConversations(conversations: List<VkConversation>)
suspend fun getConversations( suspend fun getConversations(
count: Int?, count: Int?,
offset: Int? offset: Int?,
filter: ConversationsFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain> ): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun storeConversations(conversations: List<VkConversation>) suspend fun getConversationsById(
suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain> peerIds: List<Long>,
suspend fun pin(peerId: Int): ApiResult<Int, RestApiErrorDomain> extended: Boolean? = null,
suspend fun unpin(peerId: Int): ApiResult<Int, RestApiErrorDomain> fields: String? = null
): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain>
suspend fun pin(peerId: Long): ApiResult<Int, RestApiErrorDomain>
suspend fun unpin(peerId: Long): ApiResult<Int, RestApiErrorDomain>
suspend fun reorderPinned(peerIds: List<Long>): ApiResult<Int, RestApiErrorDomain>
suspend fun archive(peerId: Long): ApiResult<Int, RestApiErrorDomain>
suspend fun unarchive(peerId: Long): ApiResult<Int, RestApiErrorDomain>
} }
@@ -1,42 +1,55 @@
package dev.meloda.fast.data.api.conversations package dev.meloda.fast.data.api.conversations
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap import dev.meloda.fast.data.VkUsersMap
import dev.meloda.fast.database.dao.ConversationDao 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.ConversationsFilter
import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.data.asDomain
import dev.meloda.fast.model.api.domain.VkConversation 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.domain.asEntity
import dev.meloda.fast.model.api.requests.ConversationsDeleteRequest
import dev.meloda.fast.model.api.requests.ConversationsGetRequest import dev.meloda.fast.model.api.requests.ConversationsGetRequest
import dev.meloda.fast.model.api.requests.ConversationsPinRequest
import dev.meloda.fast.model.api.requests.ConversationsUnpinRequest
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.conversations.ConversationsService import dev.meloda.fast.network.service.conversations.ConversationsService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ConversationsRepositoryImpl( class ConversationsRepositoryImpl(
private val conversationsService: ConversationsService, private val conversationsService: ConversationsService,
private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val conversationDao: ConversationDao private val conversationDao: ConversationDao
) : ConversationsRepository { ) : ConversationsRepository {
override suspend fun storeConversations(conversations: List<VkConversation>) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
}
override suspend fun getConversations( override suspend fun getConversations(
count: Int?, count: Int?,
offset: Int? offset: Int?,
filter: ConversationsFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsGetRequest( val requestModel = ConversationsGetRequest(
count = count, count = count,
offset = offset, offset = offset,
fields = VkConstants.ALL_FIELDS, fields = VkConstants.ALL_FIELDS,
filter = "all", filter = filter,
extended = true, extended = true,
startMessageId = null startMessageId = null
) )
@@ -56,7 +69,7 @@ class ConversationsRepositoryImpl(
VkMemoryCache.appendGroups(groupsList) VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList) VkMemoryCache.appendContacts(contactsList)
response.items.map { item -> val conversations = response.items.map { item ->
val lastMessage = item.lastMessage?.asDomain()?.let { message -> val lastMessage = item.lastMessage?.asDomain()?.let { message ->
message.copy( message.copy(
user = usersMap.messageUser(message), user = usersMap.messageUser(message),
@@ -72,6 +85,17 @@ class ConversationsRepositoryImpl(
).also { VkMemoryCache[conversation.id] = it } ).also { VkMemoryCache[conversation.id] = it }
} }
} }
val messages = conversations.mapNotNull(VkConversation::lastMessage)
launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
conversations
}, },
errorMapper = { error -> errorMapper = { error ->
error?.toDomain() error?.toDomain()
@@ -79,31 +103,93 @@ class ConversationsRepositoryImpl(
) )
} }
override suspend fun storeConversations(conversations: List<VkConversation>) { override suspend fun getConversationsById(
conversationDao.insertAll(conversations.map(VkConversation::asEntity)) peerIds: List<Long>,
extended: Boolean?,
fields: String?
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestParams = mutableMapOf(
"peer_ids" to peerIds.joinToString(separator = ",")
).apply {
extended?.let { this["extended"] = if (it) "1" else "0" }
fields?.let { this["fields"] = it }
} }
override suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain> = conversationsService.getConversationsById(requestParams).mapApiResult(
withContext(Dispatchers.IO) { successMapper = { apiResponse ->
val requestModel = ConversationsDeleteRequest(peerId = peerId) val response = apiResponse.requireResponse()
conversationsService.delete(requestModel.map).mapApiResult( val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
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) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
conversations
},
errorMapper = { error ->
error?.toDomain()
}
)
}
override suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
conversationsService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult(
successMapper = { response -> response.requireResponse().lastDeletedId }, successMapper = { response -> response.requireResponse().lastDeletedId },
errorMapper = { error -> error?.toDomain() } errorMapper = { error -> error?.toDomain() }
) )
} }
override suspend fun pin( override suspend fun pin(
peerId: Int peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsPinRequest(peerId = peerId) conversationsService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
conversationsService.pin(requestModel.map).mapApiDefault()
} }
override suspend fun unpin( override suspend fun unpin(
peerId: Int peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsUnpinRequest(peerId = peerId) conversationsService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
conversationsService.unpin(requestModel.map).mapApiDefault() }
override suspend fun reorderPinned(
peerIds: List<Long>
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService
.reorderPinned(mapOf("peer_ids" to peerIds.joinToString(",")))
.mapApiDefault()
}
override suspend fun archive(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
override suspend fun unarchive(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
} }
} }
@@ -16,7 +16,7 @@ class FilesRepository(
// AUDIO_MESSAGE("audio_message") // AUDIO_MESSAGE("audio_message")
// } // }
// //
// suspend fun getMessagesUploadServer(peerId: Int, type: FileType) = // suspend fun getMessagesUploadServer(peerid: Long, type: FileType) =
// filesService.getUploadServer( // filesService.getUploadServer(
// mapOf( // mapOf(
// "peer_id" to peerId.toString(), // "peer_id" to peerId.toString(),
@@ -8,11 +8,13 @@ import com.slack.eithernet.ApiResult
interface FriendsRepository { interface FriendsRepository {
suspend fun getAllFriends( suspend fun getAllFriends(
order: String,
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<FriendsInfo, RestApiErrorDomain> ): ApiResult<FriendsInfo, RestApiErrorDomain>
suspend fun getFriends( suspend fun getFriends(
order: String,
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<List<VkUser>, RestApiErrorDomain> ): ApiResult<List<VkUser>, RestApiErrorDomain>
@@ -20,7 +22,7 @@ interface FriendsRepository {
suspend fun getOnlineFriends( suspend fun getOnlineFriends(
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<List<Int>, RestApiErrorDomain> ): ApiResult<List<Long>, RestApiErrorDomain>
suspend fun storeUsers(users: List<VkUser>) suspend fun storeUsers(users: List<VkUser>)
} }
@@ -2,7 +2,7 @@ package dev.meloda.fast.data.api.friends
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.database.dao.UsersDao import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.FriendsInfo import dev.meloda.fast.model.FriendsInfo
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
@@ -21,14 +21,15 @@ import kotlinx.coroutines.withContext
class FriendsRepositoryImpl( class FriendsRepositoryImpl(
private val service: FriendsService, private val service: FriendsService,
private val dao: UsersDao private val dao: UserDao
) : FriendsRepository { ) : FriendsRepository {
override suspend fun getAllFriends( override suspend fun getAllFriends(
order: String,
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<FriendsInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<FriendsInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val friends = async { getFriends(count, offset) }.await() val friends = async { getFriends(order, count, offset) }.await()
.successOrElse { failure -> .successOrElse { failure ->
return@withContext failure return@withContext failure
} }
@@ -42,11 +43,12 @@ class FriendsRepositoryImpl(
} }
override suspend fun getFriends( override suspend fun getFriends(
order: String,
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = GetFriendsRequest( val requestModel = GetFriendsRequest(
order = "hints", order = order,
count = count, count = count,
offset = offset, offset = offset,
fields = VkConstants.USER_FIELDS fields = VkConstants.USER_FIELDS
@@ -67,7 +69,7 @@ class FriendsRepositoryImpl(
override suspend fun getOnlineFriends( override suspend fun getOnlineFriends(
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<Long>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = GetOnlineFriendsRequest( val requestModel = GetOnlineFriendsRequest(
order = "hints", order = "hints",
count = count, count = count,
@@ -1,77 +1,113 @@
package dev.meloda.fast.data.api.messages package dev.meloda.fast.data.api.messages
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.data.VkChatData
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
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.RestApiErrorDomain
import com.slack.eithernet.ApiResult
interface MessagesRepository { interface MessagesRepository {
suspend fun storeMessages(messages: List<VkMessage>)
suspend fun getHistory( suspend fun getHistory(
conversationId: Int, conversationId: Long,
offset: Int?, offset: Int?,
count: Int? count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> ): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
suspend fun getById( suspend fun getById(
messagesIds: List<Int>, peerCmIds: List<Long>?,
peerId: Long?,
messagesIds: List<Long>?,
cmIds: List<Long>?,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): ApiResult<List<VkMessage>, RestApiErrorDomain> ): ApiResult<List<VkMessage>, RestApiErrorDomain>
suspend fun send( suspend fun send(
peerId: Int, peerId: Long,
randomId: Int, randomId: Long,
message: String?, message: String?,
replyTo: Int?, replyTo: Long?,
attachments: List<VkAttachment>? attachments: List<VkAttachment>?,
): ApiResult<Int, RestApiErrorDomain> formatData: VkMessage.FormatData?
): ApiResult<MessagesSendResponse, RestApiErrorDomain>
suspend fun markAsRead( suspend fun markAsRead(
peerId: Int, peerId: Long,
startMessageId: Int? startMessageId: Long?
): ApiResult<Int, RestApiErrorDomain> ): ApiResult<Int, RestApiErrorDomain>
suspend fun getHistoryAttachments( suspend fun getHistoryAttachments(
peerId: Int, peerId: Long,
count: Int?, count: Int?,
offset: Int?, offset: Int?,
attachmentTypes: List<String>, attachmentTypes: List<String>,
conversationMessageId: Int cmId: Long
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> ): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain>
suspend fun storeMessages(messages: List<VkMessage>) suspend fun createChat(
userIds: List<Long>?,
title: String?
): ApiResult<Long, RestApiErrorDomain>
// suspend fun markAsImportant( suspend fun pin(
// params: MessagesMarkAsImportantRequest peerId: Long,
// ): ApiResult<List<Int>, RestApiErrorDomain> messageId: Long? = null,
// cmId: Long? = null
// suspend fun pin( ): ApiResult<VkMessage, RestApiErrorDomain>
// params: MessagesPinMessageRequest
// ): ApiResult<VkMessageData, RestApiErrorDomain> suspend fun unpin(
// peerId: Long
// suspend fun unpin( ): ApiResult<Int, RestApiErrorDomain>
// params: MessagesUnPinMessageRequest
// ): ApiResult<Unit, RestApiErrorDomain> suspend fun markAsImportant(
// peerId: Long,
// suspend fun delete( messageIds: List<Long>? = null,
// params: MessagesDeleteRequest cmIds: List<Long>? = null,
// ): ApiResult<Unit, RestApiErrorDomain> important: Boolean
// ): ApiResult<List<Long>, RestApiErrorDomain>
// suspend fun edit(
// params: MessagesEditRequest suspend fun delete(
// ): ApiResult<Int, RestApiErrorDomain> peerId: Long,
// messageIds: List<Long>?,
// suspend fun getChat( cmIds: List<Long>?,
// params: MessagesGetChatRequest spam: Boolean,
// ): ApiResult<VkChatData, RestApiErrorDomain> deleteForAll: Boolean
// ): ApiResult<List<Any>, RestApiErrorDomain>
// suspend fun getConversationMembers(
// params: MessagesGetConversationMembersRequest suspend fun edit(
// ): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> peerId: Long,
// messageId: Long? = null,
// suspend fun removeChatUser( cmId: Long? = null,
// params: MessagesRemoveChatUserRequest message: String? = null,
// ): ApiResult<Int, RestApiErrorDomain> lat: Float? = null,
long: Float? = null,
attachments: List<VkAttachment>? = null,
notParseLinks: Boolean = false,
keepSnippets: Boolean = true,
keepForwardedMessages: Boolean = true
): ApiResult<Int, RestApiErrorDomain>
suspend fun getChat(
chatId: Long,
fields: String? = null
): ApiResult<VkChatData, RestApiErrorDomain>
suspend fun getConversationMembers(
peerId: Long,
offset: Int? = null,
count: Int? = null,
extended: Boolean? = null,
fields: String? = null
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain>
suspend fun removeChatUser(
chatId: Long,
memberId: Long
): ApiResult<Int, RestApiErrorDomain>
} }
@@ -1,39 +1,61 @@
package dev.meloda.fast.data.api.messages package dev.meloda.fast.data.api.messages
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap import dev.meloda.fast.data.VkUsersMap
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.MessageDao
import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData
import dev.meloda.fast.model.api.data.VkChatData
import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.data.asDomain
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
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.VkMessage
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.model.api.domain.asEntity import dev.meloda.fast.model.api.domain.asEntity
import dev.meloda.fast.model.api.requests.MessagesCreateChatRequest
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.MessagesGetByIdRequest
import dev.meloda.fast.model.api.requests.MessagesGetChatRequest
import dev.meloda.fast.model.api.requests.MessagesGetConversationMembersRequest
import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest
import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest
import dev.meloda.fast.model.api.requests.MessagesMarkAsImportantRequest
import dev.meloda.fast.model.api.requests.MessagesMarkAsReadRequest import dev.meloda.fast.model.api.requests.MessagesMarkAsReadRequest
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.MessagesSendRequest
import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest
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.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.messages.MessagesService import dev.meloda.fast.network.service.messages.MessagesService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class MessagesRepositoryImpl( class MessagesRepositoryImpl(
private val messagesService: MessagesService, private val messagesService: MessagesService,
private val messageDao: MessageDao, private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val conversationDao: ConversationDao
) : MessagesRepository { ) : MessagesRepository {
override suspend fun getHistory( override suspend fun getHistory(
conversationId: Int, conversationId: Long,
offset: Int?, offset: Int?,
count: Int? count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
@@ -84,6 +106,13 @@ class MessagesRepositoryImpl(
} }
} }
launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
MessagesHistoryInfo( MessagesHistoryInfo(
messages = messages, messages = messages,
conversations = conversations conversations = conversations
@@ -96,12 +125,18 @@ class MessagesRepositoryImpl(
} }
override suspend fun getById( override suspend fun getById(
messagesIds: List<Int>, peerCmIds: List<Long>?,
peerId: Long?,
messagesIds: List<Long>?,
cmIds: List<Long>?,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): ApiResult<List<VkMessage>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkMessage>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetByIdRequest( val requestModel = MessagesGetByIdRequest(
peerCmIds = peerCmIds,
peerId = peerId,
messagesIds = messagesIds, messagesIds = messagesIds,
cmIds = cmIds,
extended = extended, extended = extended,
fields = fields fields = fields
) )
@@ -111,12 +146,15 @@ class MessagesRepositoryImpl(
val response = apiResponse.requireResponse() val response = apiResponse.requireResponse()
val messages = response.items val messages = response.items
val usersMap =
VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain))
val groupsMap =
VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain))
messages.map { message -> val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
val domainMessages = messages.map { message ->
message.asDomain().copy( message.asDomain().copy(
user = usersMap.messageUser(message), user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message), group = groupsMap.messageGroup(message),
@@ -124,32 +162,46 @@ class MessagesRepositoryImpl(
actionGroup = groupsMap.messageActionGroup(message) actionGroup = groupsMap.messageActionGroup(message)
) )
} }
launch(Dispatchers.IO) {
messageDao.insertAll(domainMessages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
domainMessages
}, },
errorMapper = { error -> error?.toDomain() } errorMapper = { error -> error?.toDomain() }
) )
} }
override suspend fun send( override suspend fun send(
peerId: Int, peerId: Long,
randomId: Int, randomId: Long,
message: String?, message: String?,
replyTo: Int?, replyTo: Long?,
attachments: List<VkAttachment>? attachments: List<VkAttachment>?,
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { formatData: VkMessage.FormatData?
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesSendRequest( val requestModel = MessagesSendRequest(
peerId = peerId, peerId = peerId,
randomId = randomId, randomId = randomId,
message = message, message = message,
replyTo = replyTo, replyTo = replyTo,
attachments = attachments attachments = attachments,
formatData = formatData
) )
messagesService.send(requestModel.map).mapApiDefault() messagesService.send(requestModel.map).mapApiDefault()
} }
override suspend fun markAsRead( override suspend fun markAsRead(
peerId: Int, peerId: Long,
startMessageId: Int? startMessageId: Long?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesMarkAsReadRequest( val requestModel = MessagesMarkAsReadRequest(
peerId = peerId, peerId = peerId,
@@ -160,11 +212,11 @@ class MessagesRepositoryImpl(
} }
override suspend fun getHistoryAttachments( override suspend fun getHistoryAttachments(
peerId: Int, peerId: Long,
count: Int?, count: Int?,
offset: Int?, offset: Int?,
attachmentTypes: List<String>, attachmentTypes: List<String>,
conversationMessageId: Int cmId: Long
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> = ): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryAttachmentsRequest( val requestModel = MessagesGetHistoryAttachmentsRequest(
@@ -174,7 +226,7 @@ class MessagesRepositoryImpl(
offset = offset, offset = offset,
preserveOrder = true, preserveOrder = true,
attachmentTypes = attachmentTypes, attachmentTypes = attachmentTypes,
conversationMessageId = conversationMessageId, conversationMessageId = cmId,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
) )
@@ -190,6 +242,11 @@ class MessagesRepositoryImpl(
VkMemoryCache.appendGroups(groupsList) VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList) VkMemoryCache.appendContacts(contactsList)
launch(Dispatchers.IO) {
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
response.items.map(VkAttachmentHistoryMessageData::toDomain) response.items.map(VkAttachmentHistoryMessageData::toDomain)
}, },
errorMapper = { error -> errorMapper = { error ->
@@ -198,81 +255,151 @@ class MessagesRepositoryImpl(
) )
} }
override suspend fun createChat(
userIds: List<Long>?,
title: String?
): ApiResult<Long, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesCreateChatRequest(
userIds = userIds,
title = title
)
messagesService.createChat(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
apiResponse.requireResponse().chatId
},
errorMapper = { error -> error?.toDomain() }
)
}
override suspend fun pin(
peerId: Long,
messageId: Long?,
cmId: Long?
): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesPinMessageRequest(
peerId = peerId,
messageId = messageId,
conversationMessageId = cmId
)
messagesService.pin(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
apiResponse.requireResponse().asDomain()
},
errorMapper = { error -> error?.toDomain() }
)
}
override suspend fun unpin(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesUnpinMessageRequest(peerId = peerId)
messagesService.unpin(requestModel.map).mapApiDefault()
}
override suspend fun markAsImportant(
peerId: Long,
messageIds: List<Long>?,
cmIds: List<Long>?,
important: Boolean
): ApiResult<List<Long>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesMarkAsImportantRequest(
messagesIds = messageIds.orEmpty(),
important = important
)
messagesService.markAsImportant(requestModel.map).mapApiDefault()
}
override suspend fun delete(
peerId: Long,
messageIds: List<Long>?,
cmIds: List<Long>?,
spam: Boolean,
deleteForAll: Boolean
): ApiResult<List<Any>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesDeleteRequest(
peerId = peerId,
messagesIds = messageIds,
conversationsMessagesIds = cmIds,
isSpam = spam,
deleteForAll = deleteForAll
)
messagesService.delete(requestModel.map).mapApiDefault()
}
override suspend fun storeMessages(messages: List<VkMessage>) { override suspend fun storeMessages(messages: List<VkMessage>) {
messageDao.insertAll(messages.map(VkMessage::asEntity)) messageDao.insertAll(messages.map(VkMessage::asEntity))
} }
// override suspend fun markAsImportant( override suspend fun edit(
// params: MessagesMarkAsImportantRequest peerId: Long,
// ): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) { messageId: Long?,
// messagesService.markAsImportant(params.map).mapResult( cmId: Long?,
// successMapper = { response -> response.requireResponse() }, message: String?,
// errorMapper = { error -> error?.toDomain() } lat: Float?,
// ) long: Float?,
// } attachments: List<VkAttachment>?,
// notParseLinks: Boolean,
// override suspend fun pin( keepSnippets: Boolean,
// params: MessagesPinMessageRequest keepForwardedMessages: Boolean
// ): ApiResult<VkMessageData, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.pin(params.map).mapResult( val requestModel = MessagesEditRequest(
// successMapper = { response -> response.requireResponse() }, peerId = peerId,
// errorMapper = { error -> error?.toDomain() } messageId = messageId,
// ) cmId = cmId,
// } message = message,
// lat = lat,
// override suspend fun unpin( long = long,
// params: MessagesUnPinMessageRequest attachments = attachments,
// ): ApiResult<Unit, RestApiErrorDomain> = withContext(Dispatchers.IO) { notParseLinks = notParseLinks,
// messagesService.unpin(params.map).mapResult( keepSnippets = keepSnippets,
// successMapper = {}, keepForwardedMessages = keepForwardedMessages
// errorMapper = { error -> error?.toDomain() } )
// )
// }
//
// override suspend fun delete(
// params: MessagesDeleteRequest
// ): ApiResult<Unit, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.delete(params.map).mapResult(
// successMapper = {},
// errorMapper = { error -> error?.toDomain() }
// )
// }
//
// override suspend fun edit(
// params: MessagesEditRequest
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.edit(params.map).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
// }
//
// override suspend fun getChat(
// params: MessagesGetChatRequest
// ): ApiResult<VkChatData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.getChat(params.map).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
// }
//
// override suspend fun getConversationMembers(
// params: MessagesGetConversationMembersRequest
// ): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> =
// withContext(Dispatchers.IO) {
// messagesService.getConversationMembers(params.map).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
// }
//
// override suspend fun removeChatUser(
// params: MessagesRemoveChatUserRequest
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.removeChatUser(params.map).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
// }
}
messagesService.edit(requestModel.map).mapApiDefault()
}
override suspend fun getChat(
chatId: Long,
fields: String?
): ApiResult<VkChatData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetChatRequest(
chatId = chatId,
fields = fields
)
messagesService.getChat(requestModel.map).mapApiDefault()
}
override suspend fun getConversationMembers(
peerId: Long,
offset: Int?,
count: Int?,
extended: Boolean?,
fields: String?
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
val requestModel = MessagesGetConversationMembersRequest(
peerId = peerId,
offset = offset,
count = count,
extended = extended,
fields = fields
)
messagesService.getConversationMembers(requestModel.map).mapApiDefault()
}
override suspend fun removeChatUser(
chatId: Long,
memberId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesRemoveChatUserRequest(
chatId = chatId,
memberId = memberId
)
messagesService.removeChatUser(requestModel.map).mapApiDefault()
}
}
@@ -1,6 +1,9 @@
package dev.meloda.fast.data.api.oauth package dev.meloda.fast.data.api.oauth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.responses.AuthDirectResponse import dev.meloda.fast.model.api.responses.AuthDirectResponse
import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import dev.meloda.fast.network.OAuthErrorDomain
interface OAuthRepository { interface OAuthRepository {
@@ -11,5 +14,14 @@ interface OAuthRepository {
validationCode: String?, validationCode: String?,
captchaSid: String?, captchaSid: String?,
captchaKey: String? captchaKey: String?
): AuthDirectResponse ): ApiResult<AuthDirectResponse, OAuthErrorDomain>
suspend fun getSilentToken(
login: String,
password: String,
forceSms: Boolean,
validationCode: String?,
captchaSid: String?,
captchaKey: String?,
): ApiResult<GetSilentTokenResponse, OAuthErrorDomain>
} }
@@ -1,10 +1,16 @@
package dev.meloda.fast.data.api.oauth package dev.meloda.fast.data.api.oauth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.model.api.requests.AuthDirectRequest import dev.meloda.fast.model.api.requests.AuthDirectRequest
import dev.meloda.fast.model.api.responses.AuthDirectResponse import dev.meloda.fast.model.api.responses.AuthDirectResponse
import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import dev.meloda.fast.network.OAuthErrorDomain
import dev.meloda.fast.network.ValidationType
import dev.meloda.fast.network.VkOAuthError
import dev.meloda.fast.network.VkOAuthErrorType
import dev.meloda.fast.network.mapResult
import dev.meloda.fast.network.service.oauth.OAuthService import dev.meloda.fast.network.service.oauth.OAuthService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -18,37 +24,190 @@ class OAuthRepositoryImpl(
forceSms: Boolean, forceSms: Boolean,
validationCode: String?, validationCode: String?,
captchaSid: String?, captchaSid: String?,
captchaKey: String? captchaKey: String?,
): AuthDirectResponse = withContext(Dispatchers.IO) { ): ApiResult<AuthDirectResponse, OAuthErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = AuthDirectRequest( val requestModel = AuthDirectRequest(
grantType = VkConstants.Auth.GrantType.PASSWORD, grantType = VkConstants.Auth.GrantType.PASSWORD,
clientId = VkConstants.VK_APP_ID, clientId = VkConstants.MESSENGER_APP_ID.toString(),
clientSecret = VkConstants.VK_SECRET, clientSecret = VkConstants.MESSENGER_APP_SECRET,
username = login, username = login,
password = password, password = password,
scope = VkConstants.Auth.SCOPE, scope = VkConstants.MESSENGER_APP_SCOPE.toString(),
validationForceSms = forceSms, validationForceSms = forceSms,
validationCode = validationCode, validationCode = validationCode,
captchaSid = captchaSid, captchaSid = captchaSid,
captchaKey = captchaKey, captchaKey = captchaKey,
) )
when (val result = oAuthService.auth(requestModel.map)) { oAuthService.auth(requestModel.map).mapResult(
is ApiResult.Success -> result.value successMapper = {
it
},
errorMapper = { response ->
val error = response?.error?.let(VkOAuthError::parse)
val errorType = response?.errorType?.let(VkOAuthErrorType::parse)
is ApiResult.Failure.HttpFailure -> { when (error) {
requireNotNull(result.error) null -> OAuthErrorDomain.UnknownError
VkOAuthError.FLOOD_CONTROL -> OAuthErrorDomain.TooManyTriesError
VkOAuthError.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
OAuthErrorDomain.UserBannedError(
memberName = info.memberName,
message = info.message,
accessToken = info.accessToken,
restoreUrl = info.restoreUrl
)
} else {
OAuthErrorDomain.ValidationRequiredError(
description = response.errorDescription.orEmpty(),
validationType = response.validationType.orEmpty()
.let(ValidationType::parse),
validationSid = response.validationSid.orEmpty(),
phoneMask = response.phoneMask.orEmpty(),
redirectUri = response.redirectUri.orEmpty(),
validationResend = response.validationResend,
restoreIfCannotGetCode = response.restoreIfCannotGetCode
)
}
} }
is ApiResult.Failure.ApiFailure -> TODO() VkOAuthError.NEED_CAPTCHA -> {
OAuthErrorDomain.CaptchaRequiredError(
is ApiResult.Failure.NetworkFailure -> { captchaSid = response.captchaSid.orEmpty(),
// TODO: 13/07/2024, Danil Nikolaev: implement showing network error captchaImageUrl = response.captchaImage.orEmpty()
TODO() )
} }
is ApiResult.Failure.UnknownFailure -> TODO()
else -> throw IllegalStateException("Unknown result") VkOAuthError.INVALID_CLIENT -> {
OAuthErrorDomain.InvalidCredentialsError
} }
VkOAuthError.INVALID_REQUEST -> {
when (errorType) {
null -> OAuthErrorDomain.UnknownError
VkOAuthErrorType.WRONG_OTP -> {
OAuthErrorDomain.WrongValidationCode
}
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
OAuthErrorDomain.WrongValidationCodeFormat
}
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
OAuthErrorDomain.TooManyTriesError
}
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
OAuthErrorDomain.InvalidCredentialsError
}
}
}
VkOAuthError.UNKNOWN -> OAuthErrorDomain.UnknownError
}
}
)
}
override suspend fun getSilentToken(
login: String,
password: String,
forceSms: Boolean,
validationCode: String?,
captchaSid: String?,
captchaKey: String?,
): ApiResult<GetSilentTokenResponse, OAuthErrorDomain> =
withContext(Dispatchers.IO) {
val requestModel = AuthDirectRequest(
grantType = VkConstants.Auth.GrantType.PASSWORD,
clientId = VkConstants.MESSENGER_APP_ID.toString(),
clientSecret = VkConstants.MESSENGER_APP_SECRET,
username = login,
password = password,
scope = VkConstants.MESSENGER_APP_SCOPE.toString(),
validationForceSms = forceSms,
validationCode = validationCode,
captchaSid = captchaSid,
captchaKey = captchaKey,
)
oAuthService.getSilentToken(requestModel.map).mapResult(
successMapper = { it },
errorMapper = { response ->
val error = response?.error?.let(VkOAuthError::parse)
val errorType = response?.errorType?.let(VkOAuthErrorType::parse)
when (error) {
null -> OAuthErrorDomain.UnknownError
VkOAuthError.FLOOD_CONTROL -> OAuthErrorDomain.TooManyTriesError
VkOAuthError.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
OAuthErrorDomain.UserBannedError(
memberName = info.memberName,
message = info.message,
accessToken = info.accessToken,
restoreUrl = info.restoreUrl
)
} else {
OAuthErrorDomain.ValidationRequiredError(
description = response.errorDescription.orEmpty(),
validationType = response.validationType.orEmpty()
.let(ValidationType::parse),
validationSid = response.validationSid.orEmpty(),
phoneMask = response.phoneMask.orEmpty(),
redirectUri = response.redirectUri.orEmpty(),
validationResend = response.validationResend,
restoreIfCannotGetCode = response.restoreIfCannotGetCode
)
}
}
VkOAuthError.NEED_CAPTCHA -> {
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = response.captchaSid.orEmpty(),
captchaImageUrl = response.captchaImage.orEmpty()
)
}
VkOAuthError.INVALID_CLIENT -> {
OAuthErrorDomain.InvalidCredentialsError
}
VkOAuthError.INVALID_REQUEST -> {
when (errorType) {
null -> OAuthErrorDomain.UnknownError
VkOAuthErrorType.WRONG_OTP -> {
OAuthErrorDomain.WrongValidationCode
}
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
OAuthErrorDomain.WrongValidationCodeFormat
}
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
OAuthErrorDomain.TooManyTriesError
}
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
OAuthErrorDomain.InvalidCredentialsError
}
}
}
VkOAuthError.UNKNOWN -> OAuthErrorDomain.UnknownError
}
}
)
} }
} }
@@ -8,7 +8,7 @@ class PhotosRepository(
private val photosService: PhotosService private val photosService: PhotosService
) { ) {
suspend fun getMessagesUploadServer(peerId: Int) = suspend fun getMessagesUploadServer(peerId: Long) =
photosService.getUploadServer(mapOf("peer_id" to peerId.toString())) photosService.getUploadServer(mapOf("peer_id" to peerId.toString()))
suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) =
@@ -1,18 +1,18 @@
package dev.meloda.fast.data.api.users package dev.meloda.fast.data.api.users
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import com.slack.eithernet.ApiResult
interface UsersRepository { interface UsersRepository {
suspend fun get( suspend fun get(
userIds: List<Int>?, userIds: List<Long>?,
fields: String?, fields: String?,
nomCase: String? nomCase: String?
): ApiResult<List<VkUser>, RestApiErrorDomain> ): ApiResult<List<VkUser>, RestApiErrorDomain>
suspend fun getLocalUsers(userIds: List<Int>): List<VkUser> suspend fun getLocalUsers(userIds: List<Long>): List<VkUser>
suspend fun storeUsers(users: List<VkUser>) suspend fun storeUsers(users: List<VkUser>)
} }
@@ -1,7 +1,8 @@
package dev.meloda.fast.data.api.users package dev.meloda.fast.data.api.users
import com.slack.eithernet.ApiResult
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.database.dao.UsersDao import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.model.api.domain.asEntity import dev.meloda.fast.model.api.domain.asEntity
@@ -11,18 +12,17 @@ import dev.meloda.fast.model.database.asExternalModel
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.users.UsersService import dev.meloda.fast.network.service.users.UsersService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class UsersRepositoryImpl( class UsersRepositoryImpl(
private val service: UsersService, private val service: UsersService,
private val dao: UsersDao private val dao: UserDao
) : UsersRepository { ) : UsersRepository {
override suspend fun get( override suspend fun get(
userIds: List<Int>?, userIds: List<Long>?,
fields: String?, fields: String?,
nomCase: String? nomCase: String?
): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
@@ -38,7 +38,9 @@ class UsersRepositoryImpl(
val users = response.map(VkUserData::mapToDomain) val users = response.map(VkUserData::mapToDomain)
launch { storeUsers(users) } launch(Dispatchers.IO) {
storeUsers(users)
}
VkMemoryCache.appendUsers(users) VkMemoryCache.appendUsers(users)
@@ -51,7 +53,7 @@ class UsersRepositoryImpl(
} }
override suspend fun getLocalUsers( override suspend fun getLocalUsers(
userIds: List<Int> userIds: List<Long>
): List<VkUser> = withContext(Dispatchers.IO) { ): List<VkUser> = withContext(Dispatchers.IO) {
dao.getAllByIds(userIds).map(VkUserEntity::asExternalModel) dao.getAllByIds(userIds).map(VkUserEntity::asExternalModel)
} }
@@ -6,7 +6,7 @@ interface AccountsRepository {
suspend fun getAccounts(): List<AccountEntity> suspend fun getAccounts(): List<AccountEntity>
suspend fun getAccountById(userId: Int): AccountEntity? suspend fun getAccountById(userId: Long): AccountEntity?
suspend fun storeAccounts(accounts: List<AccountEntity>) suspend fun storeAccounts(accounts: List<AccountEntity>)
} }
@@ -9,7 +9,7 @@ class AccountsRepositoryImpl(
override suspend fun getAccounts(): List<AccountEntity> = accountDao.getAll() override suspend fun getAccounts(): List<AccountEntity> = accountDao.getAll()
override suspend fun getAccountById(userId: Int): AccountEntity? = override suspend fun getAccountById(userId: Long): AccountEntity? =
accountDao.getById(userId) accountDao.getById(userId)
override suspend fun storeAccounts( override suspend fun storeAccounts(
@@ -65,7 +65,6 @@ val dataModule = module {
singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class
// TODO: 11/08/2024, Danil Nikolaev: find a better solution
single<Interceptor>(named("token_interceptor")) { single<Interceptor>(named("token_interceptor")) {
AccessTokenInterceptor() AccessTokenInterceptor()
} }
@@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 2, "version": 2,
"identityHash": "3ebd234270e36902d3d461af38664869", "identityHash": "ca007bca2ab4a9b901662792042770ad",
"entities": [ "entities": [
{ {
"tableName": "accounts", "tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, PRIMARY KEY(`userId`))", "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": [ "fields": [
{ {
"fieldPath": "userId", "fieldPath": "userId",
@@ -31,6 +31,12 @@
"columnName": "trustedHash", "columnName": "trustedHash",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
},
{
"fieldPath": "exchangeToken",
"columnName": "exchangeToken",
"affinity": "TEXT",
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@@ -46,7 +52,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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, '3ebd234270e36902d3d461af38664869')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ca007bca2ab4a9b901662792042770ad')"
] ]
} }
} }
@@ -0,0 +1,58 @@
{
"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')"
]
}
}
@@ -0,0 +1,454 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "fa307a5eb2e1f7d601bd1374174635cd",
"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",
"notNull": false
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo400Orig",
"columnName": "photo400Orig",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `conversationMessageId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "conversationMessageId",
"columnName": "conversationMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"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",
"notNull": false
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actionConversationMessageId",
"columnName": "actionConversationMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "important",
"columnName": "important",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forwardIds",
"columnName": "forwardIds",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "replyMessageId",
"columnName": "replyMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "geoType",
"columnName": "geoType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedAt",
"columnName": "pinnedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastConversationMessageId",
"columnName": "lastConversationMessageId",
"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",
"notNull": false
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
},
{
"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",
"notNull": false
},
{
"fieldPath": "peerType",
"columnName": "peerType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isArchived",
"columnName": "isArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fa307a5eb2e1f7d601bd1374174635cd')"
]
}
}
@@ -7,7 +7,7 @@ import dev.meloda.fast.model.database.AccountEntity
@Database( @Database(
entities = [AccountEntity::class], entities = [AccountEntity::class],
version = 2 version = 3
) )
abstract class AccountsDatabase : RoomDatabase() { abstract class AccountsDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao abstract fun accountDao(): AccountDao
@@ -6,7 +6,7 @@ import androidx.room.TypeConverters
import dev.meloda.fast.database.dao.ConversationDao import dev.meloda.fast.database.dao.ConversationDao
import dev.meloda.fast.database.dao.GroupDao import dev.meloda.fast.database.dao.GroupDao
import dev.meloda.fast.database.dao.MessageDao import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.database.dao.UsersDao import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.database.typeconverters.Converters import dev.meloda.fast.database.typeconverters.Converters
import dev.meloda.fast.model.database.VkConversationEntity import dev.meloda.fast.model.database.VkConversationEntity
import dev.meloda.fast.model.database.VkGroupEntity import dev.meloda.fast.model.database.VkGroupEntity
@@ -21,11 +21,11 @@ import dev.meloda.fast.model.database.VkUserEntity
VkConversationEntity::class VkConversationEntity::class
], ],
version = 7 version = 10
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class CacheDatabase : RoomDatabase() { abstract class CacheDatabase : RoomDatabase() {
abstract fun userDao(): UsersDao abstract fun userDao(): UserDao
abstract fun groupDao(): GroupDao abstract fun groupDao(): GroupDao
abstract fun messageDao(): MessageDao abstract fun messageDao(): MessageDao
abstract fun conversationDao(): ConversationDao abstract fun conversationDao(): ConversationDao
@@ -11,8 +11,8 @@ abstract class AccountDao : EntityDao<AccountEntity> {
abstract suspend fun getAll(): List<AccountEntity> abstract suspend fun getAll(): List<AccountEntity>
@Query("SELECT * FROM accounts WHERE userId = :userId") @Query("SELECT * FROM accounts WHERE userId = :userId")
abstract suspend fun getById(userId: Int): AccountEntity? abstract suspend fun getById(userId: Long): AccountEntity?
@Query("DELETE FROM accounts WHERE userId = :userId") @Query("DELETE FROM accounts WHERE userId = :userId")
abstract suspend fun deleteById(userId: Int) abstract suspend fun deleteById(userId: Long)
} }
@@ -16,11 +16,11 @@ abstract class ConversationDao : EntityDao<VkConversationEntity> {
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConversationEntity> abstract suspend fun getAllByIds(ids: List<Int>): List<VkConversationEntity>
@Query("SELECT * FROM conversations WHERE id IS (:id)") @Query("SELECT * FROM conversations WHERE id IS (:id)")
abstract suspend fun getById(id: Int): VkConversationEntity? abstract suspend fun getById(id: Long): VkConversationEntity?
@Transaction @Transaction
@Query("SELECT * FROM conversations WHERE id IS (:id)") @Query("SELECT * FROM conversations WHERE id IS (:id)")
abstract suspend fun getByIdWithMessage(id: Int): ConversationWithMessage? abstract suspend fun getByIdWithMessage(id: Long): ConversationWithMessage?
@Query("DELETE FROM conversations WHERE rowid IN (:ids)") @Query("DELETE FROM conversations WHERE rowid IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int abstract suspend fun deleteByIds(ids: List<Int>): Int
@@ -11,13 +11,13 @@ abstract class MessageDao : EntityDao<VkMessageEntity> {
abstract suspend fun getAll(): List<VkMessageEntity> abstract suspend fun getAll(): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE peerId IS (:conversationId)") @Query("SELECT * FROM messages WHERE peerId IS (:conversationId)")
abstract suspend fun getAll(conversationId: Int): List<VkMessageEntity> abstract suspend fun getAll(conversationId: Long): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE id IN (:ids)") @Query("SELECT * FROM messages WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity> abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE id IS (:messageId)") @Query("SELECT * FROM messages WHERE id IS (:messageId)")
abstract suspend fun getById(messageId: Int): VkMessageEntity? abstract suspend fun getById(messageId: Long): VkMessageEntity?
@Query("DELETE FROM messages WHERE id IN (:ids)") @Query("DELETE FROM messages WHERE id IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int abstract suspend fun deleteByIds(ids: List<Int>): Int
@@ -5,14 +5,14 @@ import androidx.room.Query
import dev.meloda.fast.model.database.VkUserEntity import dev.meloda.fast.model.database.VkUserEntity
@Dao @Dao
abstract class UsersDao : EntityDao<VkUserEntity> { abstract class UserDao : EntityDao<VkUserEntity> {
@Query("SELECT * FROM users") @Query("SELECT * FROM users")
abstract suspend fun getAll(): List<VkUserEntity> abstract suspend fun getAll(): List<VkUserEntity>
@Query("SELECT * FROM users WHERE id IN (:ids)") @Query("SELECT * FROM users WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkUserEntity> abstract suspend fun getAllByIds(ids: List<Long>): List<VkUserEntity>
@Query("DELETE FROM users WHERE id IN (:ids)") @Query("DELETE FROM users WHERE id IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int abstract suspend fun deleteByIds(ids: List<Long>): Int
} }
@@ -2,17 +2,21 @@ package dev.meloda.fast.database.di
import androidx.room.Room import androidx.room.Room
import dev.meloda.fast.database.AccountsDatabase import dev.meloda.fast.database.AccountsDatabase
import dev.meloda.fast.database.CacheDatabase
import dev.meloda.fast.database.di.migration.migrationFrom2To3
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
import org.koin.dsl.module import org.koin.dsl.module
val databaseModule = module { val databaseModule = module {
single { single {
Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts").build() Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts")
.addMigrations(migrationFrom2To3)
.build()
} }
single { get<AccountsDatabase>().accountDao() } single { get<AccountsDatabase>().accountDao() }
single { single {
Room.databaseBuilder(get(), dev.meloda.fast.database.CacheDatabase::class.java, "cache") Room.databaseBuilder(get(), CacheDatabase::class.java, "cache")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
} }
@@ -22,4 +26,4 @@ val databaseModule = module {
single { cacheDB().conversationDao() } single { cacheDB().conversationDao() }
} }
private fun Scope.cacheDB(): dev.meloda.fast.database.CacheDatabase = get() private fun Scope.cacheDB(): CacheDatabase = get()
@@ -0,0 +1,14 @@
package dev.meloda.fast.database.di.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val migrationFrom2To3 = object : Migration(
startVersion = 2,
endVersion = 3
) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE accounts ADD COLUMN exchangeToken TEXT DEFAULT null")
}
}
@@ -13,6 +13,15 @@ class Converters {
.split(", ") .split(", ")
.mapNotNull(String::toIntOrNull) .mapNotNull(String::toIntOrNull)
@TypeConverter
fun longListToString(list: List<Long>): String = list.joinToString()
@TypeConverter
fun stringToLongList(string: String): List<Long> =
string
.split(", ")
.mapNotNull(String::toLongOrNull)
@TypeConverter @TypeConverter
fun stringListToString(list: List<String>): String = list.joinToString() fun stringListToString(list: List<String>): String = list.joinToString()
@@ -89,12 +89,20 @@ object AppSettings {
) )
set(value) = put(SettingsKeys.KEY_USE_CONTACT_NAMES, value) set(value) = put(SettingsKeys.KEY_USE_CONTACT_NAMES, value)
var enablePullToRefresh: Boolean var showEmojiButton: Boolean
get() = get( get() = get(
SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH, SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
) )
set(value) = put(SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH, value) set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
var enableHaptic: Boolean
get() = get(
SettingsKeys.KEY_ENABLE_HAPTIC,
SettingsKeys.DEFAULT_ENABLE_HAPTIC
)
set(value) = put(SettingsKeys.KEY_ENABLE_HAPTIC, value)
} }
object Appearance { object Appearance {
@@ -126,6 +134,13 @@ object AppSettings {
) )
set(value) = put(SettingsKeys.KEY_USE_DYNAMIC_COLORS, value) set(value) = put(SettingsKeys.KEY_USE_DYNAMIC_COLORS, value)
var useSystemFont: Boolean
get() = get(
SettingsKeys.KEY_USE_SYSTEM_FONT,
SettingsKeys.DEFAULT_USE_SYSTEM_FONT
)
set(value) = put(SettingsKeys.KEY_USE_SYSTEM_FONT, value)
var appLanguage: String var appLanguage: String
get() = get( get() = get(
SettingsKeys.KEY_APPEARANCE_LANGUAGE, SettingsKeys.KEY_APPEARANCE_LANGUAGE,
@@ -152,6 +167,36 @@ object AppSettings {
set(value) = put(SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS, value) set(value) = put(SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS, value)
} }
object Experimental {
var longPollInBackground: Boolean
get() = get(
SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND
)
set(value) = put(SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND, value)
var showTimeInActionMessages: Boolean
get() = get(
SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES,
SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES
)
set(value) = put(SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES, value)
var useBlur: Boolean
get() = get(
SettingsKeys.KEY_USE_BLUR,
SettingsKeys.DEFAULT_USE_BLUR
)
set(value) = put(SettingsKeys.KEY_USE_BLUR, value)
var moreAnimations: Boolean
get() = get(
SettingsKeys.KEY_MORE_ANIMATIONS,
SettingsKeys.DEFAULT_MORE_ANIMATIONS
)
set(value) = put(SettingsKeys.KEY_MORE_ANIMATIONS, value)
}
object Debug { object Debug {
var showAlertAfterCrash: Boolean var showAlertAfterCrash: Boolean
get() = get( get() = get(
@@ -160,41 +205,6 @@ object AppSettings {
) )
set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value) set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value)
var longPollInBackground: Boolean
get() = get(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
set(value) = put(SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, value)
var useBlur: Boolean
get() = get(
SettingsKeys.KEY_APPEARANCE_USE_BLUR,
SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR
)
set(value) = put(SettingsKeys.KEY_APPEARANCE_USE_BLUR, value)
var showEmojiButton: Boolean
get() = get(
SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
)
set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
var showTimeInActionMessages: Boolean
get() = get(
SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES,
SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES
)
set(value) = put(SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, value)
var enableHaptic: Boolean
get() = get(
SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC,
SettingsKeys.DEFAULT_DEBUG_ENABLE_HAPTIC
)
set(value) = put(SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC, value)
var networkLogLevel: LogLevel var networkLogLevel: LogLevel
get() = get( get() = get(
SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL,
@@ -202,13 +212,6 @@ object AppSettings {
).let(LogLevel::parse) ).let(LogLevel::parse)
set(level) = put(SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, level.value) set(level) = put(SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, level.value)
var useSystemFont: Boolean
get() = get(
SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT,
SettingsKeys.DEFAULT_DEBUG_USE_SYSTEM_FONT
)
set(value) = put(SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT, value)
var showDebugCategory: Boolean var showDebugCategory: Boolean
get() = get( get() = get(
SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, SettingsKeys.KEY_SHOW_DEBUG_CATEGORY,
@@ -7,11 +7,9 @@ object SettingsKeys {
const val KEY_ACCOUNT_LOGOUT = "account_logout" const val KEY_ACCOUNT_LOGOUT = "account_logout"
const val KEY_GENERAL = "general" const val KEY_GENERAL = "general"
const val KEY_USE_CONTACT_NAMES = "general_use_contact_names" const val KEY_USE_CONTACT_NAMES = "use_contact_names"
const val DEFAULT_VALUE_USE_CONTACT_NAMES = false const val DEFAULT_VALUE_USE_CONTACT_NAMES = false
const val KEY_ENABLE_PULL_TO_REFRESH = "general_pull_to_refresh" const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button"
const val DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH = false
const val KEY_SHOW_EMOJI_BUTTON = "general_show_emoji_button"
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
const val KEY_APPEARANCE = "appearance" const val KEY_APPEARANCE = "appearance"
@@ -23,20 +21,20 @@ object SettingsKeys {
const val DEFAULT_VALUE_APPEARANCE_AMOLED_THEME = false const val DEFAULT_VALUE_APPEARANCE_AMOLED_THEME = false
const val KEY_USE_DYNAMIC_COLORS = "appearance_use_dynamic_colors" const val KEY_USE_DYNAMIC_COLORS = "appearance_use_dynamic_colors"
const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false
const val KEY_APPEARANCE_COLOR_SCHEME = "appearance_color_scheme" const val KEY_COLOR_SCHEME = "appearance_color_scheme"
const val DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME = 0 const val DEFAULT_COLOR_SCHEME = 0
const val KEY_APPEARANCE_LANGUAGE = "appearance_language" const val KEY_APPEARANCE_LANGUAGE = "appearance_language"
const val DEFAULT_APPEARANCE_LANGUAGE = "" const val DEFAULT_APPEARANCE_LANGUAGE = ""
const val KEY_APPEARANCE_USE_BLUR = "appearance_use_blur" const val KEY_USE_BLUR = "use_blur"
const val DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR = false const val DEFAULT_USE_BLUR = false
const val KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES = const val KEY_SHOW_TIME_IN_ACTION_MESSAGES =
"appearance_show_time_in_action_messages" "show_time_in_action_messages"
const val DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES = false const val DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES = false
const val KEY_FEATURES_FAST_TEXT = "features_fast_text" const val KEY_FEATURES_FAST_TEXT = "features_fast_text"
const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯" const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯"
const val KEY_FEATURES_LONG_POLL_IN_BACKGROUND = "features_lp_background" const val KEY_LONG_POLL_IN_BACKGROUND = "lp_background"
const val DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND = false const val DEFAULT_LONG_POLL_IN_BACKGROUND = false
const val KEY_ACTIVITY_SEND_ONLINE_STATUS = "activity_send_online_status" const val KEY_ACTIVITY_SEND_ONLINE_STATUS = "activity_send_online_status"
const val DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS = false const val DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS = false
@@ -44,15 +42,16 @@ object SettingsKeys {
const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash" const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash"
const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert"
const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list" const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list"
const val KEY_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages" const val KEY_ENABLE_HAPTIC = "enable_haptic"
const val KEY_DEBUG_ENABLE_HAPTIC = "debug_enable_haptic" const val DEFAULT_ENABLE_HAPTIC = true
const val DEFAULT_DEBUG_ENABLE_HAPTIC = true
const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level" const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level"
const val DEFAULT_NETWORK_LOG_LEVEL = 0 const val DEFAULT_NETWORK_LOG_LEVEL = 3
const val KEY_DEBUG_USE_SYSTEM_FONT = "debug_use_system_font" const val KEY_USE_SYSTEM_FONT = "use_system_font"
const val DEFAULT_DEBUG_USE_SYSTEM_FONT = false const val DEFAULT_USE_SYSTEM_FONT = false
const val KEY_MORE_ANIMATIONS = "more_animations"
const val DEFAULT_MORE_ANIMATIONS = false
const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category"
const val ID_DMITRY = 37610580L
const val ID_DMITRY = 37610580
} }
@@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.StateFlow
interface UserSettings { interface UserSettings {
val useContactNames: StateFlow<Boolean> val useContactNames: StateFlow<Boolean>
val enablePullToRefresh: StateFlow<Boolean>
val enableMultiline: StateFlow<Boolean> val enableMultiline: StateFlow<Boolean>
val darkMode: StateFlow<DarkMode> val darkMode: StateFlow<DarkMode>
@@ -25,10 +24,10 @@ interface UserSettings {
val showEmojiButton: StateFlow<Boolean> val showEmojiButton: StateFlow<Boolean>
val showTimeInActionMessages: StateFlow<Boolean> val showTimeInActionMessages: StateFlow<Boolean>
val useSystemFont: StateFlow<Boolean> val useSystemFont: StateFlow<Boolean>
val enableAnimations: StateFlow<Boolean>
val showDebugCategory: StateFlow<Boolean> val showDebugCategory: StateFlow<Boolean>
fun onUseContactNamesChanged(use: Boolean) fun onUseContactNamesChanged(use: Boolean)
fun onEnablePullToRefreshChanged(enable: Boolean)
fun onEnableMultilineChanged(enable: Boolean) fun onEnableMultilineChanged(enable: Boolean)
fun onDarkModeChanged(mode: DarkMode) fun onDarkModeChanged(mode: DarkMode)
@@ -52,7 +51,6 @@ interface UserSettings {
class UserSettingsImpl : UserSettings { class UserSettingsImpl : UserSettings {
override val useContactNames = MutableStateFlow(AppSettings.General.useContactNames) override val useContactNames = MutableStateFlow(AppSettings.General.useContactNames)
override val enablePullToRefresh = MutableStateFlow(AppSettings.General.enablePullToRefresh)
override val enableMultiline = MutableStateFlow(AppSettings.Appearance.enableMultiline) override val enableMultiline = MutableStateFlow(AppSettings.Appearance.enableMultiline)
override val darkMode = MutableStateFlow(AppSettings.Appearance.darkMode) override val darkMode = MutableStateFlow(AppSettings.Appearance.darkMode)
@@ -65,22 +63,19 @@ class UserSettingsImpl : UserSettings {
override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus) override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus)
override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash) override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash)
override val longPollInBackground = MutableStateFlow(AppSettings.Debug.longPollInBackground) override val longPollInBackground = MutableStateFlow(AppSettings.Experimental.longPollInBackground)
override val useBlur = MutableStateFlow(AppSettings.Debug.useBlur) override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur)
override val showEmojiButton = MutableStateFlow(AppSettings.Debug.showEmojiButton) override val showEmojiButton = MutableStateFlow(AppSettings.General.showEmojiButton)
override val showTimeInActionMessages = override val showTimeInActionMessages =
MutableStateFlow(AppSettings.Debug.showTimeInActionMessages) MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages)
override val useSystemFont = MutableStateFlow(AppSettings.Debug.useSystemFont) override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont)
override val enableAnimations = MutableStateFlow(AppSettings.Experimental.moreAnimations)
override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory) override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory)
override fun onUseContactNamesChanged(use: Boolean) { override fun onUseContactNamesChanged(use: Boolean) {
useContactNames.value = use useContactNames.value = use
} }
override fun onEnablePullToRefreshChanged(enable: Boolean) {
enablePullToRefresh.value = enable
}
override fun onEnableMultilineChanged(enable: Boolean) { override fun onEnableMultilineChanged(enable: Boolean) {
enableMultiline.value = enable enableMultiline.value = enable
} }
@@ -1,12 +1,32 @@
package dev.meloda.fast.domain package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AuthUseCase { interface AuthUseCase : BaseUseCase {
fun logout(): Flow<State<Int>>
fun validatePhone( fun validatePhone(
validationSid: String validationSid: String
): Flow<State<ValidatePhoneResponse>> ): Flow<State<ValidatePhoneResponse>>
suspend fun getAnonymToken(
clientId: String,
clientSecret: String
): Flow<State<GetAnonymTokenResponse>>
suspend fun exchangeSilentToken(
anonymToken: String,
silentToken: String,
silentUuid: String
): Flow<State<ExchangeSilentTokenResponse>>
suspend fun getExchangeToken(
accessToken: String
): Flow<State<GetExchangeTokenResponse>>
} }
@@ -3,16 +3,44 @@ package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.auth.AuthRepository import dev.meloda.fast.data.api.auth.AuthRepository
import dev.meloda.fast.data.mapToState import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class AuthUseCaseImpl(private val repository: AuthRepository) : AuthUseCase { class AuthUseCaseImpl(private val repository: AuthRepository) : AuthUseCase {
override fun validatePhone(validationSid: String): Flow<State<ValidatePhoneResponse>> = flow { override fun logout(): Flow<State<Int>> = flowNewState { repository.logout().mapToState() }
emit(State.Loading)
val newState = repository.validatePhone(validationSid).mapToState() override fun validatePhone(validationSid: String): Flow<State<ValidatePhoneResponse>> =
emit(newState) flowNewState { repository.validatePhone(validationSid = validationSid).mapToState() }
override suspend fun getAnonymToken(
clientId: String,
clientSecret: String
): Flow<State<GetAnonymTokenResponse>> = flowNewState {
repository.getAnonymToken(
clientId = clientId,
clientSecret = clientSecret
).mapToState()
}
override suspend fun exchangeSilentToken(
anonymToken: String,
silentToken: String,
silentUuid: String
): Flow<State<ExchangeSilentTokenResponse>> = flowNewState {
repository.exchangeSilentToken(
anonymToken = anonymToken,
silentToken = silentToken,
silentUuid = silentUuid
).mapToState()
}
override suspend fun getExchangeToken(
accessToken: String
): Flow<State<GetExchangeTokenResponse>> = flowNewState {
repository.getExchangeToken(accessToken = accessToken).mapToState()
} }
} }
@@ -0,0 +1,16 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
interface BaseUseCase {
suspend fun <T> FlowCollector<State<T>>.emitState(stateBlock: suspend () -> State<T>) {
emit(State.Loading)
emit(stateBlock())
}
fun <T> flowNewState(stateBlock: suspend () -> State<T>) =
flow { emitState(stateBlock) }
}
@@ -1,19 +1,29 @@
package dev.meloda.fast.domain package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ConversationsUseCase { interface ConversationsUseCase : BaseUseCase {
fun getConversations(
count: Int?,
offset: Int?,
): Flow<State<List<VkConversation>>>
fun delete(peerId: Int): Flow<State<Int>>
fun changePinState(peerId: Int, pin: Boolean): Flow<State<Int>>
suspend fun storeConversations(conversations: List<VkConversation>) suspend fun storeConversations(conversations: List<VkConversation>)
fun getConversations(
count: Int? = null,
offset: Int? = null,
filter: ConversationsFilter
): Flow<State<List<VkConversation>>>
fun getById(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): Flow<State<List<VkConversation>>>
fun delete(peerId: Long): Flow<State<Long>>
fun changePinState(peerId: Long, pin: Boolean): Flow<State<Int>>
fun changeArchivedState(peerId: Long, archive: Boolean): Flow<State<Int>>
} }
@@ -3,116 +3,69 @@ package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.conversations.ConversationsRepository import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.mapToState import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ConversationsUseCaseImpl( class ConversationsUseCaseImpl(
private val repository: ConversationsRepository, private val repository: ConversationsRepository,
) : ConversationsUseCase { ) : ConversationsUseCase {
// override fun getConversations(
// count: Int?,
// offset: Int?,
// fields: String,
// filter: String,
// extended: Boolean?,
// startMessageId: Int?
// ): Flow<dev.meloda.fast.network.State<ConversationsResponseDomain>> = flow {
// emit(dev.meloda.fast.network.State.Loading)
//
// val newState = conversationsRepository.getConversations(
// params = ConversationsGetRequest(
// count = count,
// offset = offset,
// fields = fields,
// filter = filter,
// extended = extended,
// startMessageId = startMessageId
// )
// ).fold(
// onSuccess = { response -> dev.meloda.fast.network.State.Success(response.toDomain()) },
// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError },
// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR },
// onHttpFailure = { result -> result.error.toStateApiError() },
// onApiFailure = { result -> result.error.toStateApiError() }
// )
// emit(newState)
// }
//
//
// override fun pin(peerId: Int): Flow<dev.meloda.fast.network.State<Unit>> = flow {
// emit(dev.meloda.fast.network.State.Loading)
//
// val newState = conversationsRepository.pin(
// ConversationsPinRequest(peerId = peerId)
// ).fold(
// onSuccess = { dev.meloda.fast.network.State.Success(Unit) },
// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError },
// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR },
// onHttpFailure = { result -> result.error.toStateApiError() },
// onApiFailure = { result -> result.error.toStateApiError() }
// )
// emit(newState)
// }
//
// override fun unpin(peerId: Int): Flow<dev.meloda.fast.network.State<Unit>> = flow {
// emit(dev.meloda.fast.network.State.Loading)
//
// val newState = conversationsRepository.unpin(
// ConversationsUnpinRequest(peerId = peerId)
// ).fold(
// onSuccess = { dev.meloda.fast.network.State.Success(Unit) },
// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError },
// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR },
// onHttpFailure = { result -> result.error.toStateApiError() },
// onApiFailure = { result -> result.error.toStateApiError() }
// )
// emit(newState)
// }
//
// override suspend fun storeConversations(conversations: List<VkConversationDomain>) {
// conversationsDao.insertAll(conversations.map(VkConversationDomain::mapToDb))
// }
//
// override suspend fun storeGroups(groups: List<VkGroupDomain>) {
// groupsDao.insertAll(groups.map(VkGroupDomain::mapToDB))
// }
override fun getConversations(
count: Int?,
offset: Int?
): Flow<State<List<VkConversation>>> = flow {
emit(State.Loading)
val newState = repository.getConversations(count, offset).mapToState()
emit(newState)
}
override suspend fun storeConversations( override suspend fun storeConversations(
conversations: List<VkConversation> conversations: List<VkConversation>
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
repository.storeConversations(conversations) repository.storeConversations(conversations)
} }
override fun delete(peerId: Int): Flow<State<Int>> = flow { override fun getConversations(
emit(State.Loading) count: Int?,
offset: Int?,
val newState = repository.delete(peerId = peerId).mapToState() filter: ConversationsFilter
emit(newState) ): Flow<State<List<VkConversation>>> = flowNewState {
repository.getConversations(
count = count,
offset = offset,
filter = filter
).mapToState()
} }
override fun changePinState(peerId: Int, pin: Boolean): Flow<State<Int>> = flow { override fun getById(
emit(State.Loading) peerIds: List<Long>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkConversation>>> = flowNewState {
repository.getConversationsById(
peerIds = peerIds,
extended = extended,
fields = fields
).mapToState()
}
val newState = if (pin) { override fun delete(peerId: Long): Flow<State<Long>> = flowNewState {
repository.delete(peerId = peerId).mapToState()
}
override fun changePinState(
peerId: Long,
pin: Boolean
): Flow<State<Int>> = flowNewState {
if (pin) {
repository.pin(peerId) repository.pin(peerId)
} else { } else {
repository.unpin(peerId) repository.unpin(peerId)
}.mapToState() }.mapToState()
}
emit(newState) override fun changeArchivedState(
peerId: Long,
archive: Boolean
): Flow<State<Int>> = flowNewState {
if (archive) {
repository.archive(peerId)
} else {
repository.unarchive(peerId)
}.mapToState()
} }
} }
@@ -8,11 +8,13 @@ import kotlinx.coroutines.flow.Flow
interface FriendsUseCase { interface FriendsUseCase {
fun getAllFriends( fun getAllFriends(
order: String = "hints",
count: Int?, count: Int?,
offset: Int? offset: Int?
): Flow<State<FriendsInfo>> ): Flow<State<FriendsInfo>>
fun getFriends( fun getFriends(
order: String = "hints",
count: Int?, count: Int?,
offset: Int? offset: Int?
): Flow<State<List<VkUser>>> ): Flow<State<List<VkUser>>>
@@ -20,7 +22,7 @@ interface FriendsUseCase {
fun getOnlineFriends( fun getOnlineFriends(
count: Int?, count: Int?,
offset: Int? offset: Int?
): Flow<State<List<Int>>> ): Flow<State<List<Long>>>
suspend fun storeUsers(users: List<VkUser>) suspend fun storeUsers(users: List<VkUser>)
} }
@@ -11,25 +11,32 @@ import kotlinx.coroutines.flow.flow
class FriendsUseCaseImpl(private val repository: FriendsRepository) : class FriendsUseCaseImpl(private val repository: FriendsRepository) :
FriendsUseCase { FriendsUseCase {
override fun getAllFriends(count: Int?, offset: Int?): Flow<State<FriendsInfo>> = flow { override fun getAllFriends(order: String, count: Int?, offset: Int?): Flow<State<FriendsInfo>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = repository.getAllFriends(count, offset).mapToState() val newState = repository.getAllFriends(order, count, offset).mapToState()
emit(newState) emit(newState)
} }
override fun getFriends( override fun getFriends(
count: Int?, offset: Int? order: String,
count: Int?,
offset: Int?
): Flow<State<List<VkUser>>> = flow { ): Flow<State<List<VkUser>>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = repository.getFriends(count, offset).mapToState() val newState = repository.getFriends(
order = order,
count = count,
offset = offset
).mapToState()
emit(newState) emit(newState)
} }
override fun getOnlineFriends( override fun getOnlineFriends(
count: Int?, offset: Int? count: Int?, offset: Int?
): Flow<State<List<Int>>> = flow { ): Flow<State<List<Long>>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = repository.getOnlineFriends(count, offset).mapToState() val newState = repository.getOnlineFriends(count, offset).mapToState()
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flow
class GetLocalUserByIdUseCase(private val repository: UsersRepository) { class GetLocalUserByIdUseCase(private val repository: UsersRepository) {
operator fun invoke(userId: Int): Flow<State<VkUser?>> = flow { operator fun invoke(userId: Long): Flow<State<VkUser?>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = kotlin.runCatching { val newState = kotlin.runCatching {
@@ -20,4 +20,8 @@ class GetLocalUserByIdUseCase(private val repository: UsersRepository) {
emit(newState) emit(newState)
} }
suspend fun proceed(userId: Long): VkUser? {
return repository.getLocalUsers(userIds = listOf(userId)).singleOrNull()
}
} }
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flow
class GetLocalUsersByIdsUseCase(private val repository: UsersRepository) { class GetLocalUsersByIdsUseCase(private val repository: UsersRepository) {
operator fun invoke(userIds: List<Int>): Flow<State<List<VkUser>>> = flow { operator fun invoke(userIds: List<Long>): Flow<State<List<VkUser>>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = kotlin.runCatching { val newState = kotlin.runCatching {
@@ -0,0 +1,25 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.flow.Flow
class LoadConversationsByIdUseCase(
private val conversationsRepository: ConversationsRepository
) : BaseUseCase {
operator fun invoke(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): Flow<State<List<VkConversation>>> = flowNewState {
conversationsRepository
.getConversationsById(
peerIds = peerIds,
extended = extended,
fields = fields,
).mapToState()
}
}
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.flow
class LoadUserByIdUseCase(private val repository: UsersRepository) { class LoadUserByIdUseCase(private val repository: UsersRepository) {
operator fun invoke( operator fun invoke(
userId: Int?, userId: Long?,
fields: String = VkConstants.USER_FIELDS, fields: String = VkConstants.USER_FIELDS,
nomCase: String? = null nomCase: String? = null
): Flow<State<VkUser?>> = flow { ): Flow<State<VkUser?>> = flow {
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.flow
class LoadUsersByIdsUseCase(private val repository: UsersRepository) { class LoadUsersByIdsUseCase(private val repository: UsersRepository) {
operator fun invoke( operator fun invoke(
userIds: List<Int>?, userIds: List<Long>?,
fields: String = VkConstants.USER_FIELDS, fields: String = VkConstants.USER_FIELDS,
nomCase: String? = null nomCase: String? = null
): Flow<State<List<VkUser>>> = flow { ): Flow<State<List<VkUser>>> = flow {
@@ -3,30 +3,39 @@ package dev.meloda.fast.domain
import android.util.Log import android.util.Log
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.asInt import dev.meloda.fast.common.extensions.asInt
import dev.meloda.fast.common.extensions.asLong
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.toList import dev.meloda.fast.common.extensions.toList
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.model.ApiEvent import dev.meloda.fast.model.ApiEvent
import dev.meloda.fast.model.ConversationFlags
import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.LongPollEvent import dev.meloda.fast.model.LongPollEvent
import dev.meloda.fast.model.LongPollParsedEvent
import dev.meloda.fast.model.MessageFlags
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class LongPollUpdatesParser( class LongPollUpdatesParser(
private val conversationsUseCase: ConversationsUseCase,
private val messagesUseCase: MessagesUseCase private val messagesUseCase: MessagesUseCase
) { ) {
private val job = SupervisorJob() private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> private val exceptionHandler =
Log.d("LongPollUpdatesParser", "error: $throwable") CoroutineExceptionHandler { _, throwable ->
Log.e("LongPollUpdatesParser", "error: $throwable")
throwable.printStackTrace() throwable.printStackTrace()
} }
@@ -35,29 +44,26 @@ class LongPollUpdatesParser(
private val coroutineScope = CoroutineScope(coroutineContext) private val coroutineScope = CoroutineScope(coroutineContext)
private val listenersMap: MutableMap<ApiEvent, MutableCollection<VkEventCallback<*>>> = private val listenersMap: MutableMap<LongPollEvent, MutableList<VkEventCallback<LongPollParsedEvent>>> =
mutableMapOf() mutableMapOf()
fun parseNextUpdate(event: List<Any>) { fun parseNextUpdate(event: List<Any>) {
val eventId = event.first().asInt() val eventId = event.first().asInt()
val eventType: ApiEvent = try { when (val eventType = ApiEvent.parseOrNull(eventId)) {
ApiEvent.parse(eventId) null -> Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
} catch (e: Exception) {
e.printStackTrace()
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
return
}
when (eventType) {
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event) ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event) ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event)
ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event) ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event)
ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event) ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event)
ApiEvent.CHAT_CLEAR_FLAGS -> parseChatClearFlags(eventType, event)
ApiEvent.CHAT_SET_FLAGS -> parseChatSetFlags(eventType, event)
ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event) ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event)
ApiEvent.PIN_UNPIN_CONVERSATION -> parseConversationPinStateChanged(eventType, event) ApiEvent.CHAT_MAJOR_CHANGED -> parseChatMajorChanged(eventType, event)
ApiEvent.CHAT_MINOR_CHANGED -> parseChatMinorChanged(eventType, event)
ApiEvent.TYPING, ApiEvent.TYPING,
ApiEvent.AUDIO_MESSAGE_RECORDING, ApiEvent.AUDIO_MESSAGE_RECORDING,
@@ -65,12 +71,460 @@ class LongPollUpdatesParser(
ApiEvent.VIDEO_UPLOADING, ApiEvent.VIDEO_UPLOADING,
ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event) ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event)
ApiEvent.UNREAD_COUNT_UPDATE -> onNewEvent(eventType, event) ApiEvent.UNREAD_COUNT_UPDATE -> parseUnreadCounterUpdate(eventType, event)
ApiEvent.MESSAGE_UPDATED -> parseMessageUpdated(eventType, event)
ApiEvent.MESSAGE_CACHE_CLEAR -> parseMessageCacheClear(eventType, event)
} }
} }
private fun onNewEvent(eventType: ApiEvent, event: List<Any>) { private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event") Log.d("LongPollUpdatesParser", "$eventType: $event")
val cmId = event[1].asLong()
val flags = event[2].asInt()
val peerId = event[3].asLong()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = MessageFlags.parse(flags)
parsedFlags.forEach { flag ->
when (flag) {
MessageFlags.IMPORTANT -> {
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
peerId = peerId,
cmId = cmId,
marked = true
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.SPAM -> {
val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam(
peerId = peerId,
cmId = cmId
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsSpam>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.DELETED -> {
val eventToSend =
if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) {
LongPollParsedEvent.MessageDeleted(
peerId = peerId,
cmId = cmId,
forAll = true
)
} else {
LongPollParsedEvent.MessageDeleted(
peerId = peerId,
cmId = cmId,
forAll = false
)
}
eventsToSend += eventToSend
listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageDeleted>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.AUDIO_LISTENED -> {
val eventToSend = LongPollParsedEvent.AudioMessageListened(
peerId = peerId,
cmId = cmId
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.AudioMessageListened>)
?.onEvent(eventToSend)
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(eventToSend)
}
}
}
}
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val cmId = event[1].asLong()
val flags = event[2].asInt()
val peerId = event[3].asLong()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = MessageFlags.parse(flags)
coroutineScope.launch {
parsedFlags.forEach { flag ->
when (flag) {
MessageFlags.IMPORTANT -> {
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
peerId = peerId,
cmId = cmId,
marked = false
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.SPAM -> {
if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) {
withContext(Dispatchers.IO) {
val message = loadMessage(
peerId = peerId,
cmId = cmId
)
message?.let {
val eventToSend =
LongPollParsedEvent.MessageMarkedAsNotSpam(message = message)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsNotSpam>)
?.onEvent(eventToSend)
}
}
}
}
}
}
MessageFlags.DELETED -> {
withContext(Dispatchers.IO) {
val message = loadMessage(
peerId = peerId,
cmId = cmId
)
message?.let {
val eventToSend =
LongPollParsedEvent.MessageRestored(message = message)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageRestored>)
?.onEvent(eventToSend)
}
}
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
vkEventCallback.onEvent(eventToSend)
}
}
}
}
}
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val cmId = event[1].asLong()
val peerId = event[4].asLong()
coroutineScope.launch(Dispatchers.IO) {
val message =
async { loadMessage(peerId = peerId, cmId = cmId) }.await()
val conversation =
async {
loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
)
}.await()
message?.let {
listenersMap[LongPollEvent.MESSAGE_NEW]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.NewMessage>)
.onEvent(
LongPollParsedEvent.NewMessage(
message = message,
inArchive = conversation?.isArchived == true
// TODO: 03-Apr-25, Danil Nikolaev:
// load user settings about restoring chats with
// enabled notifications from archive
)
)
}
}
}
}
}
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val cmId = event[1].asLong()
val peerId = event[3].asLong()
coroutineScope.launch(Dispatchers.IO) {
loadMessage(
peerId = peerId,
cmId = cmId
)?.let { message ->
listenersMap[LongPollEvent.MESSAGE_EDITED]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageEdited>)
.onEvent(LongPollParsedEvent.MessageEdited(message))
}
}
}
}
}
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
val unreadCount = event[3].asInt()
listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.IncomingMessageRead>)
.onEvent(
LongPollParsedEvent.IncomingMessageRead(
peerId = peerId,
cmId = cmId,
unreadCount = unreadCount
)
)
}
}
}
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
val unreadCount = event[3].asInt()
listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.OutgoingMessageRead>)
.onEvent(
LongPollParsedEvent.OutgoingMessageRead(
peerId = peerId,
cmId = cmId,
unreadCount = unreadCount
)
)
}
}
}
private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val flags = event[2].asInt()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConversationFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag ->
when (flag) {
ConversationFlags.ARCHIVED -> {
val conversation = loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
) ?: return@forEach
val message = loadMessage(
peerId = peerId,
cmId = conversation.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
conversation = conversation.copy(lastMessage = message),
archived = false
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.ChatArchived>)
?.onEvent(eventToSend)
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.CHAT_CLEAR_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(
eventToSend
)
}
}
}
}
}
private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val flags = event[2].asInt()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConversationFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag ->
when (flag) {
ConversationFlags.ARCHIVED -> {
val conversation = loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
) ?: return@forEach
val message = loadMessage(
peerId = peerId,
cmId = conversation.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
conversation = conversation.copy(lastMessage = message),
archived = true
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.ChatArchived>)
?.onEvent(eventToSend)
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.CHAT_SET_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(
eventToSend
)
}
}
}
}
}
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatCleared>)
.onEvent(
LongPollParsedEvent.ChatCleared(
peerId = peerId,
toCmId = cmId
)
)
}
}
}
private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val majorId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMajorChanged>)
.onEvent(
LongPollParsedEvent.ChatMajorChanged(
peerId = peerId,
majorId = majorId,
)
)
}
}
}
private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val minorId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMinorChanged>)
.onEvent(
LongPollParsedEvent.ChatMinorChanged(
peerId = peerId,
minorId = minorId,
)
)
}
}
} }
private fun parseInteraction(eventType: ApiEvent, event: List<Any>) { private fun parseInteraction(eventType: ApiEvent, event: List<Any>) {
@@ -85,20 +539,28 @@ class LongPollUpdatesParser(
else -> return else -> return
} }
val peerId = event[1].asInt() val longPollEvent: LongPollEvent = when (eventType) {
val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId } ApiEvent.TYPING -> LongPollEvent.TYPING
ApiEvent.AUDIO_MESSAGE_RECORDING -> LongPollEvent.AUDIO_MESSAGE_RECORDING
ApiEvent.PHOTO_UPLOADING -> LongPollEvent.PHOTO_UPLOADING
ApiEvent.VIDEO_UPLOADING -> LongPollEvent.VIDEO_UPLOADING
ApiEvent.FILE_UPLOADING -> LongPollEvent.FILE_UPLOADING
else -> return
}
val peerId = event[1].asLong()
val userIds = event[2].toList(Any::asLong).filter { it != UserConfig.userId }
val totalCount = event[3].asInt() val totalCount = event[3].asInt()
val timestamp = event[4].asInt() val timestamp = event[4].asInt()
// if userIds contains only account's id, then we don't need to show our status // if userIds contains only account's id, then we don't need to show our status
if (userIds.isEmpty()) return if (userIds.isEmpty()) return
coroutineScope.launch { listenersMap[longPollEvent]?.let { listeners ->
listenersMap[eventType]?.let { listeners ->
listeners.forEach { vkEventCallback -> listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.Interaction>) (vkEventCallback as VkEventCallback<LongPollParsedEvent.Interaction>)
.onEvent( .onEvent(
LongPollEvent.Interaction( LongPollParsedEvent.Interaction(
interactionType = interactionType, interactionType = interactionType,
peerId = peerId, peerId = peerId,
userIds = userIds, userIds = userIds,
@@ -109,253 +571,237 @@ class LongPollUpdatesParser(
} }
} }
} }
}
private fun parseConversationPinStateChanged(eventType: ApiEvent, event: List<Any>) { private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event") Log.d("LongPollUpdatesParser", "$eventType $event")
val peerId = event[1].asInt() val unreadCount = event[1].asInt()
val majorId = event[2].asInt() val unreadUnmutedCount = event[2].asInt()
val showOnlyMuted = event[3].asInt() == 1
val businessNotifyUnreadCount = event[4].asInt()
val archiveUnreadCount = event[7].asInt()
val archiveUnreadUnmutedCount = event[8].asInt()
val archiveMentionsCount = event[9].asInt()
coroutineScope.launch { listenersMap[LongPollEvent.UNREAD_COUNTER_UPDATE]?.let { listeners ->
listenersMap[ApiEvent.PIN_UNPIN_CONVERSATION]?.let { listeners ->
listeners.forEach { vkEventCallback -> listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) (vkEventCallback as VkEventCallback<LongPollParsedEvent.UnreadCounter>)
.onEvent( .onEvent(
LongPollEvent.VkConversationPinStateChangedEvent( LongPollParsedEvent.UnreadCounter(
peerId = peerId, unread = unreadCount,
majorId = majorId unreadUnmuted = unreadUnmutedCount,
showOnlyMuted = showOnlyMuted,
business = businessNotifyUnreadCount,
archive = archiveUnreadCount,
archiveUnmuted = archiveUnreadUnmutedCount,
archiveMentions = archiveMentionsCount
) )
) )
} }
} }
} }
}
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) { private fun parseMessageUpdated(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event") Log.d("LongPollUpdatesParser", "$eventType $event")
}
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) { val cmId = event[1].asLong()
Log.d("LongPollUpdatesParser", "$eventType: $event") val peerId = event[4].asLong()
}
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt()
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
val newMessageEvent: LongPollEvent.VkMessageNewEvent? = loadMessage(
loadNormalMessage(
eventType,
messageId
)
newMessageEvent?.let { event ->
listenersMap[ApiEvent.MESSAGE_NEW]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
.onEvent(event)
}
}
}
}
}
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt()
coroutineScope.launch {
val editedMessageEvent: LongPollEvent.VkMessageEditEvent? =
loadNormalMessage(
eventType,
messageId
)
editedMessageEvent?.let { event ->
listenersMap[ApiEvent.MESSAGE_EDIT]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
.onEvent(event)
}
}
}
}
}
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt()
val messageId = event[2].asInt()
val unreadCount = event[3].asInt()
coroutineScope.launch {
listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
.onEvent(
LongPollEvent.VkMessageReadIncomingEvent(
peerId = peerId, peerId = peerId,
messageId = messageId, cmId = cmId
unreadCount = unreadCount )?.let { message ->
) listenersMap[LongPollEvent.MESSAGE_UPDATED]?.let {
) it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageUpdated>)
.onEvent(LongPollParsedEvent.MessageUpdated(message))
}
} }
} }
} }
} }
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) { private fun parseMessageCacheClear(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event") Log.d("LongPollUpdatesParser", "$eventType $event")
val peerId = event[1].asInt()
val messageId = event[2].asInt()
val unreadCount = event[3].asInt()
coroutineScope.launch { val messageId = event[1].asLong()
listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners ->
listeners.map { vkEventCallback -> coroutineScope.launch(Dispatchers.IO) {
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) loadMessage(messageId = messageId)?.let { message ->
.onEvent( listenersMap[LongPollEvent.MESSAGE_CACHE_CLEAR]?.let {
LongPollEvent.VkMessageReadOutgoingEvent( it.map { vkEventCallback ->
peerId = peerId, (vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageCacheClear>)
messageId = messageId, .onEvent(LongPollParsedEvent.MessageCacheClear(message))
unreadCount = unreadCount }
)
)
} }
} }
} }
} }
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) { private suspend fun loadMessage(
Log.d("LongPollUpdatesParser", "$eventType: $event") peerId: Long? = null,
} cmId: Long? = null,
messageId: Long? = null
): VkMessage? = suspendCoroutine { continuation ->
require((peerId != null && cmId != null) || messageId != null)
private suspend inline fun <reified T : LongPollEvent> loadNormalMessage(
eventType: ApiEvent,
messageId: Int
): T? = suspendCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
messagesUseCase.getById( messagesUseCase.getById(
messageIds = listOf(messageId), peerCmIds = null,
peerId = peerId,
messageIds = messageId?.let(::listOf),
cmIds = cmId?.let(::listOf),
extended = true, extended = true,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
).listenValue(this) { state -> ).listenValue(this) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error") Log.e("LongPollUpdatesParser", "loadMessage: error: $error")
continuation.resume(null)
}, },
success = { messages -> success = { response ->
val message = messages.singleOrNull() ?: run { val message = response.singleOrNull() ?: run {
continuation.resume(null) continuation.resume(null)
return@listenValue return@listenValue
} }
VkMemoryCache[message.id] = message continuation.resume(message)
messagesUseCase.storeMessage(message)
val resumeValue: LongPollEvent? = when (eventType) {
ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message)
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message)
else -> {
continuation.resume(null)
null
}
}
resumeValue?.let { value -> continuation.resume(value as T) }
} }
) )
} }
} }
} }
private fun <T : Any> registerListener( private suspend fun loadConversation(
eventType: ApiEvent, peerId: Long,
listener: VkEventCallback<T> extended: Boolean = false,
) { fields: String? = null
listenersMap.let { map -> ): VkConversation? = suspendCoroutine { continuation ->
map[eventType] = (map[eventType] ?: mutableListOf()).also { it.add(listener) } coroutineScope.launch(Dispatchers.IO) {
conversationsUseCase.getById(
peerIds = listOf(peerId),
extended = extended,
fields = fields
).listenValue(coroutineScope) { state ->
state.processState(
error = { error ->
Log.e("LongPollUpdatesParser", "loadConversation: error: $error")
continuation.resume(null)
},
success = { response ->
val conversation = response.singleOrNull() ?: run {
continuation.resume(null)
return@listenValue
}
continuation.resume(conversation)
}
)
}
} }
} }
private fun <T : Any> registerListeners( @Suppress("UNCHECKED_CAST")
eventTypes: List<ApiEvent>, private fun <T : LongPollParsedEvent> registerListener(
eventType: LongPollEvent,
listener: VkEventCallback<T>
) {
listenersMap.let { map ->
map[eventType] = (map[eventType] ?: mutableListOf())
.also {
it.add(listener as VkEventCallback<LongPollParsedEvent>)
}
}
}
private fun <T : LongPollParsedEvent> registerListeners(
eventTypes: List<LongPollEvent>,
listener: VkEventCallback<T> listener: VkEventCallback<T>
) { ) {
eventTypes.forEach { eventType -> registerListener(eventType, listener) } eventTypes.forEach { eventType -> registerListener(eventType, listener) }
} }
fun onConversationPinStateChanged(listener: VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) { fun onMessageSetFlags(block: (LongPollParsedEvent) -> Unit) {
registerListener(ApiEvent.PIN_UNPIN_CONVERSATION, listener) registerListener(LongPollEvent.MESSAGE_SET_FLAGS, assembleEventCallback(block))
} }
fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) { fun onMessageMarkedAsImportant(block: (LongPollParsedEvent.MessageMarkedAsImportant) -> Unit) {
onConversationPinStateChanged(assembleEventCallback(block)) registerListener(LongPollEvent.MARKED_AS_IMPORTANT, assembleEventCallback(block))
} }
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) { fun onMessageMarkedAsSpam(block: (LongPollParsedEvent.MessageMarkedAsSpam) -> Unit) {
registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener) registerListener(LongPollEvent.MARKED_AS_SPAM, assembleEventCallback(block))
} }
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) { fun onMessageDeleted(block: (LongPollParsedEvent.MessageDeleted) -> Unit) {
onMessageIncomingRead(assembleEventCallback(block)) registerListener(LongPollEvent.MESSAGE_DELETED, assembleEventCallback(block))
} }
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) { fun onMessageClearFlags(block: (LongPollParsedEvent) -> Unit) {
registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener) registerListener(LongPollEvent.MESSAGE_CLEAR_FLAGS, assembleEventCallback(block))
} }
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) { fun onMessageMarkedAsNotSpam(block: (LongPollParsedEvent.MessageMarkedAsNotSpam) -> Unit) {
onMessageOutgoingRead(assembleEventCallback(block)) registerListener(LongPollEvent.MARKED_AS_NOT_SPAM, assembleEventCallback(block))
} }
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) { fun onMessageRestored(block: (LongPollParsedEvent.MessageRestored) -> Unit) {
registerListener(ApiEvent.MESSAGE_NEW, listener) registerListener(LongPollEvent.MESSAGE_RESTORED, assembleEventCallback(block))
} }
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) { fun onNewMessage(block: (LongPollParsedEvent.NewMessage) -> Unit) {
onNewMessage(assembleEventCallback(block)) registerListener(LongPollEvent.MESSAGE_NEW, assembleEventCallback(block))
} }
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) { fun onMessageEdited(block: (LongPollParsedEvent.MessageEdited) -> Unit) {
registerListener(ApiEvent.MESSAGE_EDIT, listener) registerListener(LongPollEvent.MESSAGE_EDITED, assembleEventCallback(block))
} }
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) { fun onMessageIncomingRead(block: (LongPollParsedEvent.IncomingMessageRead) -> Unit) {
onMessageEdited(assembleEventCallback(block)) registerListener(LongPollEvent.INCOMING_MESSAGE_READ, assembleEventCallback(block))
} }
fun onInteractions(listener: VkEventCallback<LongPollEvent.Interaction>) { fun onMessageOutgoingRead(block: (LongPollParsedEvent.OutgoingMessageRead) -> Unit) {
registerListener(LongPollEvent.OUTGOING_MESSAGE_READ, assembleEventCallback(block))
}
fun onChatCleared(block: (LongPollParsedEvent.ChatCleared) -> Unit) {
registerListener(LongPollEvent.CHAT_CLEARED, assembleEventCallback(block))
}
fun onChatMajorChanged(block: (LongPollParsedEvent.ChatMajorChanged) -> Unit) {
registerListener(LongPollEvent.CHAT_MAJOR_CHANGED, assembleEventCallback(block))
}
fun onChatMinorChanged(block: (LongPollParsedEvent.ChatMinorChanged) -> Unit) {
registerListener(LongPollEvent.CHAT_MINOR_CHANGED, assembleEventCallback(block))
}
fun onChatArchived(block: (LongPollParsedEvent.ChatArchived) -> Unit) {
registerListener(LongPollEvent.CHAT_ARCHIVED, assembleEventCallback(block))
}
fun onInteractions(block: (LongPollParsedEvent.Interaction) -> Unit) {
registerListeners( registerListeners(
eventTypes = listOf( eventTypes = listOf(
ApiEvent.TYPING, LongPollEvent.TYPING,
ApiEvent.AUDIO_MESSAGE_RECORDING, LongPollEvent.AUDIO_MESSAGE_RECORDING,
ApiEvent.PHOTO_UPLOADING, LongPollEvent.PHOTO_UPLOADING,
ApiEvent.VIDEO_UPLOADING, LongPollEvent.VIDEO_UPLOADING,
ApiEvent.FILE_UPLOADING LongPollEvent.FILE_UPLOADING
), ),
listener = listener listener = assembleEventCallback(block)
) )
} }
fun onInteractions(block: (LongPollEvent.Interaction) -> Unit) {
onInteractions(assembleEventCallback(block))
}
fun clearListeners() {
listenersMap.clear()
}
} }
internal inline fun <R : Any> assembleEventCallback( internal inline fun <R : LongPollParsedEvent> assembleEventCallback(
crossinline block: (R) -> Unit, crossinline block: (R) -> Unit,
): VkEventCallback<R> { ): VkEventCallback<R> {
return VkEventCallback { event -> block.invoke(event) } return VkEventCallback { event -> block.invoke(event) }
} }
fun interface VkEventCallback<in T : Any> { fun interface VkEventCallback<in T : LongPollParsedEvent> {
fun onEvent(event: T) fun onEvent(event: T)
} }
@@ -5,43 +5,78 @@ import dev.meloda.fast.data.api.messages.MessagesHistoryInfo
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface MessagesUseCase { interface MessagesUseCase : BaseUseCase {
suspend fun storeMessage(message: VkMessage)
suspend fun storeMessages(messages: List<VkMessage>)
fun getMessagesHistory( fun getMessagesHistory(
conversationId: Int, conversationId: Long,
count: Int?, count: Int?,
offset: Int? offset: Int?
): Flow<State<MessagesHistoryInfo>> ): Flow<State<MessagesHistoryInfo>>
fun getById( fun getById(
messageIds: List<Int>, peerCmIds: List<Long>?,
peerId: Long?,
messageIds: List<Long>?,
cmIds: List<Long>?,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): Flow<State<List<VkMessage>>> ): Flow<State<List<VkMessage>>>
fun sendMessage( fun sendMessage(
peerId: Int, peerId: Long,
randomId: Int, randomId: Long,
message: String?, message: String?,
replyTo: Int?, replyTo: Long?,
attachments: List<VkAttachment>? attachments: List<VkAttachment>?,
): Flow<State<Int>> formatData: VkMessage.FormatData?
): Flow<State<MessagesSendResponse>>
fun markAsRead( fun markAsRead(
peerId: Int, peerId: Long,
startMessageId: Int startMessageId: Long
): Flow<State<Int>> ): Flow<State<Int>>
fun getHistoryAttachments( fun getHistoryAttachments(
peerId: Int, peerId: Long,
count: Int?, count: Int? = null,
offset: Int?, offset: Int? = null,
attachmentTypes: List<String>, attachmentTypes: List<String>,
conversationMessageId: Int cmId: Long
): Flow<State<List<VkAttachmentHistoryMessage>>> ): Flow<State<List<VkAttachmentHistoryMessage>>>
suspend fun storeMessage(message: VkMessage) fun createChat(
suspend fun storeMessages(messages: List<VkMessage>) userIds: List<Long>? = null,
title: String
): Flow<State<Long>>
fun pin(
peerId: Long,
messageId: Long? = null,
cmId: Long? = null
): Flow<State<VkMessage>>
fun unpin(
peerId: Long
): Flow<State<Int>>
fun markAsImportant(
peerId: Long,
messageIds: List<Long>? = null,
cmIds: List<Long>? = null,
important: Boolean
): Flow<State<List<Long>>>
fun delete(
peerId: Long,
messageIds: List<Long>? = null,
cmIds: List<Long>? = null,
spam: Boolean = false,
deleteForAll: Boolean = false
): Flow<State<List<Any>>>
} }
@@ -7,99 +7,13 @@ import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class MessagesUseCaseImpl( class MessagesUseCaseImpl(
private val repository: MessagesRepository private val repository: MessagesRepository,
) : MessagesUseCase { ) : MessagesUseCase {
override fun getMessagesHistory(
conversationId: Int,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>> = flow {
emit(State.Loading)
val newState = repository.getHistory(
conversationId = conversationId,
offset = offset,
count = count
).mapToState()
emit(newState)
}
override fun getById(
messageIds: List<Int>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkMessage>>> = flow {
emit(State.Loading)
val newState = repository.getById(
messagesIds = messageIds,
extended = extended,
fields = fields
).mapToState()
emit(newState)
}
override fun sendMessage(
peerId: Int,
randomId: Int,
message: String?,
replyTo: Int?,
attachments: List<VkAttachment>?
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.send(
peerId = peerId,
randomId = randomId,
message = message,
replyTo = replyTo,
attachments = attachments
).mapToState()
emit(newState)
}
override fun markAsRead(
peerId: Int,
startMessageId: Int
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.markAsRead(
peerId = peerId,
startMessageId = startMessageId
).mapToState()
emit(newState)
}
override fun getHistoryAttachments(
peerId: Int,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
conversationMessageId: Int
): Flow<State<List<VkAttachmentHistoryMessage>>> = flow {
emit(State.Loading)
val newState = repository.getHistoryAttachments(
peerId = peerId,
count = count,
offset = offset,
attachmentTypes = attachmentTypes,
conversationMessageId = conversationMessageId
).mapToState()
emit(newState)
}
override suspend fun storeMessage(message: VkMessage) { override suspend fun storeMessage(message: VkMessage) {
repository.storeMessages(listOf(message)) repository.storeMessages(listOf(message))
} }
@@ -107,4 +21,131 @@ class MessagesUseCaseImpl(
override suspend fun storeMessages(messages: List<VkMessage>) { override suspend fun storeMessages(messages: List<VkMessage>) {
repository.storeMessages(messages) repository.storeMessages(messages)
} }
override fun getMessagesHistory(
conversationId: Long,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>> = flowNewState {
repository.getHistory(
conversationId = conversationId,
offset = offset,
count = count
).mapToState()
}
override fun getById(
peerCmIds: List<Long>?,
peerId: Long?,
messageIds: List<Long>?,
cmIds: List<Long>?,
extended: Boolean?,
fields: String?
): Flow<State<List<VkMessage>>> = flowNewState {
repository.getById(
peerCmIds = peerCmIds,
peerId = peerId,
messagesIds = messageIds,
cmIds = cmIds,
extended = extended,
fields = fields
).mapToState()
}
override fun sendMessage(
peerId: Long,
randomId: Long,
message: String?,
replyTo: Long?,
attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData?
): Flow<State<MessagesSendResponse>> = flowNewState {
repository.send(
peerId = peerId,
randomId = randomId,
message = message,
replyTo = replyTo,
attachments = attachments,
formatData = formatData
).mapToState()
}
override fun markAsRead(
peerId: Long,
startMessageId: Long
): Flow<State<Int>> = flowNewState {
repository.markAsRead(
peerId = peerId,
startMessageId = startMessageId
).mapToState()
}
override fun getHistoryAttachments(
peerId: Long,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
cmId: Long
): Flow<State<List<VkAttachmentHistoryMessage>>> = flowNewState {
repository.getHistoryAttachments(
peerId = peerId,
count = count,
offset = offset,
attachmentTypes = attachmentTypes,
cmId = cmId
).mapToState()
}
override fun createChat(
userIds: List<Long>?,
title: String
): Flow<State<Long>> = flowNewState {
repository.createChat(userIds, title).mapToState()
}
override fun pin(
peerId: Long,
messageId: Long?,
cmId: Long?
): Flow<State<VkMessage>> = flowNewState {
repository.pin(
peerId = peerId,
messageId = messageId,
cmId = cmId
).mapToState()
}
override fun unpin(peerId: Long): Flow<State<Int>> = flowNewState {
repository.unpin(peerId = peerId).mapToState()
}
override fun markAsImportant(
peerId: Long,
messageIds: List<Long>?,
cmIds: List<Long>?,
important: Boolean
): Flow<State<List<Long>>> = flowNewState {
repository.markAsImportant(
peerId = peerId,
messageIds = messageIds,
cmIds = cmIds,
important = important
).mapToState()
}
override fun delete(
peerId: Long,
messageIds: List<Long>?,
cmIds: List<Long>?,
spam: Boolean,
deleteForAll: Boolean
): Flow<State<List<Any>>> = flowNewState {
repository.delete(
peerId = peerId,
messageIds = messageIds,
cmIds = cmIds,
spam = spam,
deleteForAll = deleteForAll
).mapToState()
}
} }
@@ -2,6 +2,7 @@ package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.model.AuthInfo import dev.meloda.fast.model.AuthInfo
import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface OAuthUseCase { interface OAuthUseCase {
@@ -14,4 +15,13 @@ interface OAuthUseCase {
captchaSid: String?, captchaSid: String?,
captchaKey: String? captchaKey: String?
): Flow<State<AuthInfo>> ): Flow<State<AuthInfo>>
fun getSilentToken(
login: String,
password: String,
forceSms: Boolean,
validationCode: String?,
captchaSid: String?,
captchaKey: String?
): Flow<State<GetSilentTokenResponse>>
} }
@@ -2,11 +2,9 @@ package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.oauth.OAuthRepository import dev.meloda.fast.data.api.oauth.OAuthRepository
import dev.meloda.fast.data.asState
import dev.meloda.fast.model.AuthInfo import dev.meloda.fast.model.AuthInfo
import dev.meloda.fast.network.OAuthErrorDomain import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import dev.meloda.fast.network.ValidationType
import dev.meloda.fast.network.VkOAuthError
import dev.meloda.fast.network.VkOAuthErrorType
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -24,100 +22,44 @@ class OAuthUseCaseImpl(
): Flow<State<AuthInfo>> = flow { ): Flow<State<AuthInfo>> = flow {
emit(State.Loading) emit(State.Loading)
val response = oAuthRepository.auth( val newState = oAuthRepository.auth(
login = login, login = login,
password = password, password = password,
forceSms = forceSms,
validationCode = validationCode, validationCode = validationCode,
captchaSid = captchaSid, captchaSid = captchaSid,
captchaKey = captchaKey, captchaKey = captchaKey
forceSms = forceSms ).asState(
) successMapper = {
val error = response.error?.let(VkOAuthError::parse)
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
val newState = when (error) {
null -> {
State.Success(
AuthInfo( AuthInfo(
userId = response.userId, userId = it.userId!!,
accessToken = response.accessToken, accessToken = it.accessToken!!,
validationHash = response.validationHash validationHash = it.validationHash!!
)
) )
} }
VkOAuthError.FLOOD_CONTROL -> {
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
}
VkOAuthError.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
State.Error.OAuthError(
OAuthErrorDomain.UserBannedError(
memberName = info.memberName,
message = info.message,
accessToken = info.accessToken,
restoreUrl = info.restoreUrl
) )
)
} else { emit(newState)
State.Error.OAuthError(
OAuthErrorDomain.ValidationRequiredError(
description = response.errorDescription.orEmpty(),
validationType = response.validationType.orEmpty()
.let(ValidationType::parse),
validationSid = response.validationSid.orEmpty(),
phoneMask = response.phoneMask.orEmpty(),
redirectUri = response.redirectUri.orEmpty(),
validationResend = response.validationResend,
restoreIfCannotGetCode = response.restoreIfCannotGetCode
)
)
}
} }
VkOAuthError.NEED_CAPTCHA -> { override fun getSilentToken(
State.Error.OAuthError( login: String,
OAuthErrorDomain.CaptchaRequiredError( password: String,
captchaSid = response.captchaSid.orEmpty(), forceSms: Boolean,
captchaImageUrl = response.captchaImage.orEmpty() validationCode: String?,
) captchaSid: String?,
) captchaKey: String?
} ): Flow<State<GetSilentTokenResponse>> = flow {
emit(State.Loading)
VkOAuthError.INVALID_CLIENT -> { val newState = oAuthRepository.getSilentToken(
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError) login = login,
} password = password,
forceSms = forceSms,
VkOAuthError.INVALID_REQUEST -> { validationCode = validationCode,
when (errorType) { captchaSid = captchaSid,
null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError) captchaKey = captchaKey
).asState()
VkOAuthErrorType.WRONG_OTP -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
}
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat)
}
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
}
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
}
}
}
VkOAuthError.UNKNOWN -> {
State.Error.OAuthError(OAuthErrorDomain.UnknownError)
}
}
emit(newState) emit(newState)
} }
@@ -6,6 +6,7 @@ import dev.meloda.fast.domain.AccountUseCaseImpl
import dev.meloda.fast.domain.GetCurrentAccountUseCase import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.GetLocalUserByIdUseCase import dev.meloda.fast.domain.GetLocalUserByIdUseCase
import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.domain.LoadUsersByIdsUseCase import dev.meloda.fast.domain.LoadUsersByIdsUseCase
import dev.meloda.fast.domain.StoreUsersUseCase import dev.meloda.fast.domain.StoreUsersUseCase
@@ -24,4 +25,6 @@ val domainModule = module {
singleOf(::AccountUseCaseImpl) bind AccountUseCase::class singleOf(::AccountUseCaseImpl) bind AccountUseCase::class
singleOf(::GetCurrentAccountUseCase) singleOf(::GetCurrentAccountUseCase)
singleOf(::LoadConversationsByIdUseCase)
} }
@@ -1,9 +1,9 @@
package dev.meloda.fast.friends.util package dev.meloda.fast.domain.util
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.friends.model.UiFriend
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.ui.model.api.UiFriend
fun VkUser.asPresentation( fun VkUser.asPresentation(
useContactNames: Boolean = false useContactNames: Boolean = false
@@ -16,5 +16,7 @@ fun VkUser.asPresentation(
fullName fullName
}, },
onlineStatus = onlineStatus, onlineStatus = onlineStatus,
photo400Orig = photo400Orig?.let(UiImage::Url) photo400Orig = photo400Orig?.let(UiImage::Url),
firstName = firstName,
lastName = lastName
) )
@@ -1,14 +1,19 @@
package dev.meloda.fast.model package dev.meloda.fast.model
enum class ApiEvent(val value: Int) { enum class ApiEvent(val value: Int) {
MESSAGE_SET_FLAGS(2), MESSAGE_SET_FLAGS(10002),
MESSAGE_CLEAR_FLAGS(3), MESSAGE_CLEAR_FLAGS(10003),
MESSAGE_NEW(4), MESSAGE_NEW(10004),
MESSAGE_EDIT(5), MESSAGE_EDIT(10005),
MESSAGE_READ_INCOMING(6), MESSAGE_READ_INCOMING(10006),
MESSAGE_READ_OUTGOING(7), MESSAGE_READ_OUTGOING(10007),
MESSAGES_DELETED(13), CHAT_CLEAR_FLAGS(10),
PIN_UNPIN_CONVERSATION(20), CHAT_SET_FLAGS(12),
MESSAGES_DELETED(10013),
MESSAGE_UPDATED(10018),
MESSAGE_CACHE_CLEAR(10019),
CHAT_MAJOR_CHANGED(20),
CHAT_MINOR_CHANGED(21),
TYPING(63), TYPING(63),
AUDIO_MESSAGE_RECORDING(64), AUDIO_MESSAGE_RECORDING(64),
PHOTO_UPLOADING(65), PHOTO_UPLOADING(65),
@@ -18,5 +23,6 @@ enum class ApiEvent(val value: Int) {
companion object { companion object {
fun parse(value: Int) = entries.first { it.value == value } fun parse(value: Int) = entries.first { it.value == value }
fun parseOrNull(value: Int) = entries.firstOrNull { it.value == value }
} }
} }
@@ -1,7 +1,7 @@
package dev.meloda.fast.model package dev.meloda.fast.model
data class AuthInfo( data class AuthInfo(
val userId: Int?, val userId: Long,
val accessToken: String?, val accessToken: String,
val validationHash: String? val validationHash: String
) )
@@ -6,4 +6,10 @@ import androidx.compose.runtime.Immutable
sealed class BaseError { sealed class BaseError {
data object SessionExpired : BaseError() data object SessionExpired : BaseError()
data object AccountBlocked : BaseError()
data object ConnectionError : BaseError()
data object InternalError : BaseError()
data object UnknownError : BaseError()
data class SimpleError(val message: String) : BaseError()
} }
@@ -0,0 +1,32 @@
package dev.meloda.fast.model
enum class ConversationFlags(val value: Int) {
DISABLE_PUSH(16),
DISABLE_SOUND(32),
INCOMING_CHAT_REQUEST(256),
DECLINED_CHAT_REQUEST(512),
MENTION(1024),
HIDE_CHAT_FROM_SEARCH(2048),
BUSINESS_CHAT(8192),
MARKED_MESSAGE(16384), // mention or disappearing message
DO_NOT_NOTIFY_MENTIONS_ALL_ONLINE(262144),
DO_NOT_NOTIFY_ALL_MENTIONS(524288),
MARKED_AS_UNREAD(1048576),
ARCHIVED(8388608),
CALL_IN_PROGRESS(16777216);
companion object {
fun parse(mask: Int): List<ConversationFlags> {
val flags = mutableListOf<ConversationFlags>()
ConversationFlags.entries.forEach { flag ->
if (mask and flag.value > 0) {
flags.add(flag)
}
}
return flags
}
}
}
@@ -0,0 +1,5 @@
package dev.meloda.fast.model
enum class ConversationsFilter {
ALL, UNREAD, ARCHIVE, BUSINESS_NOTIFY
}
@@ -4,5 +4,5 @@ import dev.meloda.fast.model.api.domain.VkUser
data class FriendsInfo( data class FriendsInfo(
val friends: List<VkUser>, val friends: List<VkUser>,
val onlineFriendsIds: List<Int> val onlineFriendsIds: List<Long>
) )
@@ -1,35 +1,30 @@
package dev.meloda.fast.model package dev.meloda.fast.model
import dev.meloda.fast.model.api.domain.VkMessage enum class LongPollEvent {
MESSAGE_SET_FLAGS,
sealed interface LongPollEvent { MESSAGE_CLEAR_FLAGS,
MESSAGE_NEW,
data class VkMessageNewEvent(val message: VkMessage) : LongPollEvent MESSAGE_EDITED,
INCOMING_MESSAGE_READ,
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent OUTGOING_MESSAGE_READ,
CHAT_SET_FLAGS,
data class VkMessageReadIncomingEvent( CHAT_CLEAR_FLAGS,
val peerId: Int, CHAT_MAJOR_CHANGED,
val messageId: Int, CHAT_MINOR_CHANGED,
val unreadCount: Int, TYPING,
) : LongPollEvent AUDIO_MESSAGE_RECORDING,
PHOTO_UPLOADING,
data class VkMessageReadOutgoingEvent( VIDEO_UPLOADING,
val peerId: Int, FILE_UPLOADING,
val messageId: Int, UNREAD_COUNTER_UPDATE,
val unreadCount: Int, MARKED_AS_IMPORTANT,
) : LongPollEvent MARKED_AS_SPAM,
MARKED_AS_NOT_SPAM,
data class VkConversationPinStateChangedEvent( MESSAGE_DELETED,
val peerId: Int, MESSAGE_UPDATED,
val majorId: Int, MESSAGE_CACHE_CLEAR,
) : LongPollEvent MESSAGE_RESTORED,
AUDIO_MESSAGE_LISTENED,
data class Interaction( CHAT_CLEARED,
val interactionType: InteractionType, CHAT_ARCHIVED
val peerId: Int,
val userIds: List<Int>,
val totalCount: Int,
val timestamp: Int
) : LongPollEvent
} }
@@ -0,0 +1,98 @@
package dev.meloda.fast.model
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage
sealed interface LongPollParsedEvent {
data class NewMessage(
val message: VkMessage,
val inArchive: Boolean
) : LongPollParsedEvent
data class MessageEdited(val message: VkMessage) : LongPollParsedEvent
data class MessageUpdated(val message: VkMessage) : LongPollParsedEvent
data class MessageCacheClear(val message: VkMessage) : LongPollParsedEvent
data class IncomingMessageRead(
val peerId: Long,
val cmId: Long,
val unreadCount: Int,
) : LongPollParsedEvent
data class OutgoingMessageRead(
val peerId: Long,
val cmId: Long,
val unreadCount: Int,
) : LongPollParsedEvent
data class ChatMajorChanged(
val peerId: Long,
val majorId: Int,
) : LongPollParsedEvent
data class ChatMinorChanged(
val peerId: Long,
val minorId: Int
) : LongPollParsedEvent
data class Interaction(
val interactionType: InteractionType,
val peerId: Long,
val userIds: List<Long>,
val totalCount: Int,
val timestamp: Int
) : LongPollParsedEvent
data class UnreadCounter(
val unread: Int,
val unreadUnmuted: Int,
val showOnlyMuted: Boolean,
val business: Int,
val archive: Int,
val archiveUnmuted: Int,
val archiveMentions: Int
) : LongPollParsedEvent
data class MessageMarkedAsImportant(
val peerId: Long,
val cmId: Long,
val marked: Boolean
) : LongPollParsedEvent
data class MessageMarkedAsSpam(
val peerId: Long,
val cmId: Long
) : LongPollParsedEvent
data class MessageMarkedAsNotSpam(
val message: VkMessage
) : LongPollParsedEvent
data class MessageDeleted(
val peerId: Long,
val cmId: Long,
val forAll: Boolean
) : LongPollParsedEvent
data class MessageRestored(
val message: VkMessage
) : LongPollParsedEvent
data class AudioMessageListened(
val peerId: Long,
val cmId: Long
) : LongPollParsedEvent
data class ChatCleared(
val peerId: Long,
val toCmId: Long
) : LongPollParsedEvent
data class ChatArchived(
val conversation: VkConversation,
val archived: Boolean
) : LongPollParsedEvent
}
@@ -0,0 +1,31 @@
package dev.meloda.fast.model
enum class MessageFlags(val value: Int) {
UNREAD(1),
OUTGOING(2),
IMPORTANT(8),
SPAM(64),
DELETED(128),
AUDIO_LISTENED(4096),
FROM_GROUP_CHAT(8192),
CANCEL_SPAM(32768),
DELETED_FOR_ALL(131072),
DO_NOT_SHOW_NOTIFICATION(1048576),
MESSAGE_WITH_REPLY(2097152),
REACTION(16777216);
companion object {
fun parse(mask: Int): List<MessageFlags> {
val flags = mutableListOf<MessageFlags>()
entries.forEach { flag ->
if (mask and flag.value > 0) {
flags.add(flag)
}
}
return flags
}
}
}
@@ -0,0 +1,8 @@
package dev.meloda.fast.model
data class PhotoSize(
val height: Int,
val width: Int,
val type: String,
val url: String
)
@@ -6,8 +6,8 @@ enum class AttachmentType(var value: String) {
UNKNOWN("unknown"), UNKNOWN("unknown"),
PHOTO("photo"), PHOTO("photo"),
VIDEO("video"), VIDEO("video"),
AUDIO("audio"),
FILE("doc"), FILE("doc"),
AUDIO("audio"),
LINK("link"), LINK("link"),
AUDIO_MESSAGE("audio_message"), AUDIO_MESSAGE("audio_message"),
MINI_APP("mini_app"), MINI_APP("mini_app"),
@@ -27,7 +27,11 @@ enum class AttachmentType(var value: String) {
AUDIO_PLAYLIST("audio_playlist"), AUDIO_PLAYLIST("audio_playlist"),
PODCAST("podcast"), PODCAST("podcast"),
NARRATIVE("narrative"), NARRATIVE("narrative"),
ARTICLE("article"); ARTICLE("article"),
VIDEO_MESSAGE("video_message"),
GROUP_CHAT_STICKER("ugc_sticker"),
STICKER_PACK_PREVIEW("sticker_pack_preview")
;
fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE) fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE)
@@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkArticleData( data class VkArticleData(
@Json(name = "id") val id: Int @Json(name = "id") val id: Long
) : VkAttachmentData { ) : VkAttachmentData {
fun toDomain(): VkArticleDomain = VkArticleDomain( fun toDomain(): VkArticleDomain = VkArticleDomain(
@@ -1,15 +1,15 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAttachmentHistoryMessageData( data class VkAttachmentHistoryMessageData(
@Json(name = "message_id") val messageId: Int, @Json(name = "message_id") val messageId: Long,
@Json(name = "date") val date: Int, @Json(name = "date") val date: Int,
@Json(name = "cmid") val conversationMessageId: Int, @Json(name = "cmid") val conversationMessageId: Long,
@Json(name = "from_id") val fromId: Int, @Json(name = "from_id") val fromId: Long,
@Json(name = "position") val position: Int, @Json(name = "position") val position: Int,
@Json(name = "attachment") val attachment: VkAttachmentItemData @Json(name = "attachment") val attachment: VkAttachmentItemData
) { ) {
@@ -1,9 +1,9 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkUnknownAttachment
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkUnknownAttachment
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAttachmentItemData( data class VkAttachmentItemData(
@@ -32,7 +32,10 @@ data class VkAttachmentItemData(
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?, @Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
@Json(name = "podcast") val podcast: VkPodcastData?, @Json(name = "podcast") val podcast: VkPodcastData?,
@Json(name = "narrative") val narrative: VkNarrativeData?, @Json(name = "narrative") val narrative: VkNarrativeData?,
@Json(name = "article") val article: VkArticleData? @Json(name = "article") val article: VkArticleData?,
@Json(name = "video_message") val videoMessage: VkVideoMessageData?,
@Json(name = "ugc_sticker") val groupSticker: VkGroupStickerData?,
@Json(name = "sticker_pack_preview") val stickerPackPreview: VkStickerPackPreviewData?
) { ) {
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) { fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
AttachmentType.UNKNOWN -> VkUnknownAttachment AttachmentType.UNKNOWN -> VkUnknownAttachment
@@ -60,5 +63,8 @@ data class VkAttachmentItemData(
AttachmentType.PODCAST -> podcast?.toDomain() AttachmentType.PODCAST -> podcast?.toDomain()
AttachmentType.NARRATIVE -> narrative?.toDomain() AttachmentType.NARRATIVE -> narrative?.toDomain()
AttachmentType.ARTICLE -> article?.toDomain() AttachmentType.ARTICLE -> article?.toDomain()
AttachmentType.VIDEO_MESSAGE -> videoMessage?.toDomain()
AttachmentType.GROUP_CHAT_STICKER -> groupSticker?.toDomain()
AttachmentType.STICKER_PACK_PREVIEW -> stickerPackPreview?.toDomain()
} ?: VkUnknownAttachment } ?: VkUnknownAttachment
} }
@@ -1,33 +1,33 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkAudioDomain
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkAudioDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAudioData( data class VkAudioData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "artist") val artist: String, @Json(name = "artist") val artist: String,
@Json(name = "duration") val duration: Int, @Json(name = "duration") val duration: Int,
@Json(name = "url") val url: String, @Json(name = "url") val url: String,
@Json(name = "date") val date: Int, @Json(name = "date") val date: Int,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "access_key") val accessKey: String?, @Json(name = "access_key") val accessKey: String?,
@Json(name = "is_explicit") val isExplicit: Boolean, @Json(name = "is_explicit") val isExplicit: Boolean,
@Json(name = "is_focus_track") val isFocusTrack: Boolean, @Json(name = "is_focus_track") val isFocusTrack: Boolean,
@Json(name = "is_licensed") val isLicensed: Boolean?, @Json(name = "is_licensed") val isLicensed: Boolean?,
@Json(name = "genre_id") val genreId: Int?, @Json(name = "genre_id") val genreId: Long?,
@Json(name = "album") val album: Album?, @Json(name = "album") val album: Album?,
) : VkAttachmentData { ) : VkAttachmentData {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Album( data class Album(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "access_key") val accessKey: String, @Json(name = "access_key") val accessKey: String,
@Json(name = "thumb") val thumb: Thumb @Json(name = "thumb") val thumb: Thumb?
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAudioMessageData( data class VkAudioMessageData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "duration") val duration: Int, @Json(name = "duration") val duration: Int,
@Json(name = "waveform") val waveform: List<Int>, @Json(name = "waveform") val waveform: List<Int>,
@Json(name = "link_ogg") val linkOgg: String, @Json(name = "link_ogg") val linkOgg: String,
@@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAudioPlaylistData( data class VkAudioPlaylistData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "type") val type: Int, @Json(name = "type") val type: Int,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "description") val description: String, @Json(name = "description") val description: String,
@@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkCallData( data class VkCallData(
@Json(name = "initiator_id") val initiatorId: Int, @Json(name = "initiator_id") val initiatorId: Long,
@Json(name = "receiver_id") val receiverId: Int, @Json(name = "receiver_id") val receiverId: Long,
@Json(name = "state") val state: String, @Json(name = "state") val state: String,
@Json(name = "time") val time: Int, @Json(name = "time") val time: Int,
@Json(name = "duration") val duration: Int, @Json(name = "duration") val duration: Int,
@@ -8,9 +8,9 @@ import com.squareup.moshi.JsonClass
data class VkChatData( data class VkChatData(
@Json(name = "type") val type: String, @Json(name = "type") val type: String,
@Json(name = "val title") val title: String, @Json(name = "val title") val title: String,
@Json(name = "admin_id") val adminId: Int, @Json(name = "admin_id") val adminId: Long,
@Json(name = "members_count") val membersCount: Int, @Json(name = "members_count") val membersCount: Int,
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "photo_50") val photo50: String, @Json(name = "photo_50") val photo50: String,
@Json(name = "photo_100") val photo100: String, @Json(name = "photo_100") val photo100: String,
@Json(name = "photo_200") val photo200: String, @Json(name = "photo_200") val photo200: String,
@@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkChatMemberData( data class VkChatMemberData(
@Json(name = "member_id") val memberId: Int, @Json(name = "member_id") val memberId: Long,
@Json(name = "invited_by") val invitedBy: Int, @Json(name = "invited_by") val invitedBy: Int,
@Json(name = "join_date") val joinDate: Int, @Json(name = "join_date") val joinDate: Int,
@Json(name = "is_admin") val isAdmin: Boolean?, @Json(name = "is_admin") val isAdmin: Boolean?,
@@ -6,10 +6,10 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkContactData( data class VkContactData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "can_write") val canWrite: Boolean, @Json(name = "can_write") val canWrite: Boolean,
@Json(name = "user_id") val userId: Int, @Json(name = "user_id") val userId: Long,
@Json(name = "last_seen_status") val lastSeenStatus: String?, @Json(name = "last_seen_status") val lastSeenStatus: String?,
@Json(name = "photo_50") val photo50: String?, @Json(name = "photo_50") val photo50: String?,
@Json(name = "calls_id") val callsId: String @Json(name = "calls_id") val callsId: String
@@ -1,21 +1,21 @@
package dev.meloda.fast.model.api.data 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.PeerType
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkConversationData( data class VkConversationData(
@Json(name = "peer") val peer: Peer, @Json(name = "peer") val peer: Peer,
@Json(name = "last_message_id") val lastMessageId: Int?, @Json(name = "last_message_id") val lastMessageId: Long?,
@Json(name = "in_read") val inRead: Int, @Json(name = "in_read") val inRead: Long,
@Json(name = "out_read") val outRead: Int, @Json(name = "out_read") val outRead: Long,
@Json(name = "in_read_cmid") val inReadConversationMessageId: Int, @Json(name = "in_read_cmid") val inReadConversationMessageId: Long,
@Json(name = "out_read_cmid") val outReadConversationMessageId: Int, @Json(name = "out_read_cmid") val outReadConversationMessageId: Long,
@Json(name = "sort_id") val sortId: SortId, @Json(name = "sort_id") val sortId: SortId,
@Json(name = "last_conversation_message_id") val lastConversationMessageId: Int, @Json(name = "last_conversation_message_id") val lastConversationMessageId: Long,
@Json(name = "is_marked_unread") val isMarkedUnread: Boolean, @Json(name = "is_marked_unread") val isMarkedUnread: Boolean,
@Json(name = "important") val important: Boolean, @Json(name = "important") val important: Boolean,
@Json(name = "push_settings") val pushSettings: PushSettings?, @Json(name = "push_settings") val pushSettings: PushSettings?,
@@ -25,13 +25,14 @@ data class VkConversationData(
@Json(name = "chat_settings") val chatSettings: ChatSettings?, @Json(name = "chat_settings") val chatSettings: ChatSettings?,
@Json(name = "call_in_progress") val callInProgress: CallInProgress?, @Json(name = "call_in_progress") val callInProgress: CallInProgress?,
@Json(name = "unread_count") val unreadCount: Int?, @Json(name = "unread_count") val unreadCount: Int?,
@Json(name = "is_archived") val isArchived: Boolean?
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Peer( data class Peer(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "type") val type: String, @Json(name = "type") val type: String,
@Json(name = "local_id") val localId: Int, @Json(name = "local_id") val localId: Long,
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@@ -55,7 +56,7 @@ data class VkConversationData(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ChatSettings( data class ChatSettings(
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "state") val state: String, @Json(name = "state") val state: String,
@Json(name = "acl") val acl: Acl, @Json(name = "acl") val acl: Acl,
@@ -119,7 +120,7 @@ data class VkConversationData(
photo200 = chatSettings?.photo?.photo200, photo200 = chatSettings?.photo?.photo200,
isCallInProgress = callInProgress != null, isCallInProgress = callInProgress != null,
isPhantom = chatSettings?.isDisappearing == true, isPhantom = chatSettings?.isDisappearing == true,
lastConversationMessageId = lastConversationMessageId, lastCmId = lastConversationMessageId,
inRead = inRead, inRead = inRead,
outRead = outRead, outRead = outRead,
lastMessageId = lastMessageId, lastMessageId = lastMessageId,
@@ -140,5 +141,6 @@ data class VkConversationData(
pinnedMessage = chatSettings?.pinnedMessage?.mapToDomain(), pinnedMessage = chatSettings?.pinnedMessage?.mapToDomain(),
user = null, user = null,
group = null, group = null,
isArchived = isArchived == true
) )
} }
@@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkCuratorData( data class VkCuratorData(
val id: Int, val id: Long,
val name: String, val name: String,
val description: String, val description: String,
val url: String, val url: String,
@@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkEventData( data class VkEventData(
@Json(name = "button_text") val buttonText: String, @Json(name = "button_text") val buttonText: String,
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "is_favorite") val isFavorite: Boolean, @Json(name = "is_favorite") val isFavorite: Boolean,
@Json(name = "text") val text: String, @Json(name = "text") val text: String,
@Json(name = "address") val address: String, @Json(name = "address") val address: String,

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