12 Commits

Author SHA1 Message Date
melod1n bb40d1b36c Merge branch 'master' into dev 2025-03-23 17:55:40 +03:00
melod1n e645448852 more improvements 2025-03-23 17:37:13 +03:00
melod1n 314ff806c0 improvements in longpoll's stuff 2025-03-23 17:27:46 +03:00
melod1n 3beb382334 Merge remote-tracking branch 'origin/dev' into dev 2025-03-23 11:53:51 +03:00
melod1n 5b5ba747d8 some updates 2025-03-23 11:53:28 +03:00
melod1n b8937a1590 Merge branch 'master' into dev 2025-03-23 09:23:38 +03:00
melod1n a4feb8978f message sending status 2025-03-23 09:08:29 +03:00
melod1n 79f539a27b - read indicator, edit status and time for message in messages history 2025-03-23 08:45:01 +03:00
melod1n 4cc6ec6b5d Chat creation feature (#138) 2025-03-23 07:33:58 +03:00
melod1n 36a119ffa9 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`.
2025-03-21 12:43:22 +03:00
melod1n 1a78a51017 * fixed visual bug in progress bar in chat history 2025-03-21 04:49:17 +03:00
melod1n cbe3313b87 * pagination in chat fixed
* other fixes and improvements
2025-03-21 04:44:09 +03:00
564 changed files with 10190 additions and 21485 deletions
-56
View File
@@ -1,56 +0,0 @@
name: Android CI
on:
workflow_dispatch:
permissions:
contents: read
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:
android:
runs-on: android-jdk21
name: Build artifacts
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Make Gradle executable
run: chmod +x ./gradlew
- name: Build and sign release APK
run: ./gradlew assembleRelease
- name: Find generated release APK name
id: find_apk_release
run: |
APK_PATH=$(find app/build/outputs/apk/release -name "*.apk" | head -n 1)
echo "APK_PATH=$APK_PATH" >> $GITEA_ENV
echo "APK_NAME=$(basename $APK_PATH)" >> $GITEA_ENV
- name: Upload APK with original name
uses: christopherhx/gitea-upload-artifact@v4
with:
name: ${{ env.APK_NAME }}
path: ${{ env.APK_PATH }}
- name: Build and sign debug APK
run: ./gradlew assembleDebug
- name: Find generated debug APK name
id: find_apk_debug
run: |
APK_PATH=$(find app/build/outputs/apk/debug -name "*.apk" | head -n 1)
echo "APK_PATH=$APK_PATH" >> $GITEA_ENV
echo "APK_NAME=$(basename $APK_PATH)" >> $GITEA_ENV
- name: Upload APK with original name
uses: christopherhx/gitea-upload-artifact@v4
with:
name: ${{ env.APK_NAME }}
path: ${{ env.APK_PATH }}
+22 -36
View File
@@ -1,10 +1,10 @@
name: Android CI Build name: Android CI Build
on: on:
workflow_dispatch: push:
branches: [ "dev", "release/*", "hotfix/*" ]
permissions: pull_request:
contents: read branches: [ "dev", "release/*", "hotfix/*" ]
env: env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -12,51 +12,37 @@ env:
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }} RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
jobs: jobs:
build_apks: build_apk_aab:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
name: Build artifacts name: Build artifacts
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: set up JDK 21 - name: set up JDK 17
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
java-version: '21' java-version: '17'
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 release APK
run: ./gradlew assembleRelease
- name: Find generated release APK name
id: find_apk_release
run: |
APK_PATH=$(find app/build/outputs/apk/release -name "*.apk" | head -n 1)
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
- name: Upload APK with original name
uses: actions/upload-artifact@v7
with:
name: ${{ env.APK_NAME }}
path: ${{ env.APK_PATH }}
- name: Build and sign debug APK - name: Build and sign debug APK
run: ./gradlew assembleDebug run: ./gradlew assembleDebug
- name: Find generated debug APK name - name: Upload debug APK
id: find_apk_debug uses: actions/upload-artifact@v4
run: |
APK_PATH=$(find app/build/outputs/apk/debug -name "*.apk" | head -n 1)
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
- name: Upload APK with original name
uses: actions/upload-artifact@v7
with: with:
name: ${{ env.APK_NAME }} name: app-debug.apk
path: ${{ env.APK_PATH }} path: app/build/outputs/apk/debug/app-debug.apk
- name: Build and sign release APK
run: ./gradlew assembleRelease
- name: Upload release APK
uses: actions/upload-artifact@v4
with:
name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk
+26 -11
View File
@@ -1,11 +1,8 @@
name: Android CI Release name: Android CI Release
permissions:
contents: read
on: on:
workflow_dispatch: pull_request:
push: branches: [ "master" ]
branches: [ "release/*"]
env: env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -18,32 +15,50 @@ jobs:
name: Build artifacts name: Build artifacts
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: set up JDK 21 - name: set up JDK 17
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
java-version: '21' java-version: '17'
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 APK
run: ./gradlew assembleDebug
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug.apk
path: app/build/outputs/apk/debug/app-debug.apk
- name: Build and sign release APK - name: Build and sign release APK
run: ./gradlew assembleRelease run: ./gradlew assembleRelease
- name: Upload release APK - name: Upload release APK
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
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 Bundle
run: ./gradlew bundleDebug
- name: Upload debug Bundle
uses: actions/upload-artifact@v4
with:
name: app-debug.aab
path: app/build/outputs/bundle/debug/app-debug.aab
- name: Build and sign release Bundle - name: Build and sign release Bundle
run: ./gradlew bundleRelease run: ./gradlew bundleRelease
- name: Upload release Bundle - name: Upload release Bundle
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
with: with:
name: app-release.aab name: app-release.aab
path: app/build/outputs/bundle/release/app-release.aab path: app/build/outputs/bundle/release/app-release.aab
-2
View File
@@ -15,5 +15,3 @@ build/
local.properties local.properties
.idea .idea
/.kotlin /.kotlin
.hotswan/
.java-version
+22 -35
View File
@@ -7,65 +7,52 @@ Unofficial messenger for russian social network VKontakte
- [x] 2FA support - [x] 2FA support
- [x] Resend otp - [x] Resend otp
- [x] Captcha support - [x] Captcha support
- [x] Support for new authorization with service and refresh tokens - [ ] Support for new authorization with service and refresh tokens
- [ ] Handle token expiration
- [x] Ability to export/import tokens
- [x] Conversations list - [x] Conversations list
- [x] Pagination - [x] Pagination
- [x] Manual refresh - [x] Manual refresh
- [x] Pin & unpin conversations - [x] Pin & unpin conversations
- [x] Delete conversations - [x] Delete conversations
- [x] Archive - [ ] Archive
- [x] View archived conversations - [ ] View archived conversations
- [x] Archive & unarchive conversations - [ ] Archive & unarchive conversations
- [x] Friends list - [x] Friends list
- [x] Sort alphabetically, by priority or random - [ ] Sort alphabetically, by priority or random
- [x] Separate tab with only friends who are online - [ ] Separate tab with only friends who are online
- [x] Settings screen - [x] Settings screen
- [ ] TODO - [ ] TODO
- [x] Chat screen - [x] Chat screen
- [x] Pagination - [ ] Pagination
- [x] Manual refresh - [x] Manual refresh
- [x] Message bubbles - [x] Message bubbles
- [x] Text - [x] Text
- [x] Date - [ ] Date
- [x] Read status - [ ] Message's attachments
- [x] Edit status - [ ] Photo
- [x] Sending status - [ ] Video
- [x] Message's attachments - [ ] Audio
- [x] Photo - [ ] File
- [x] Video - [ ] Link
- [x] Audio
- [x] File
- [x] Link
- [x] Sticker
- [x] Reply
- [ ] Forwarded messages
- [ ] Wall post
- [ ] Comment in wall post
- [ ] Comment in channel
- [ ] Poll
- [ ] TODO - [ ] TODO
- [x] Send messages - [x] Send messages
- [x] Pinned message - [ ] Pinned message
- [x] Pin & unpin messages - [ ] Pin & unpin messages
- [x] Reply to message - [ ] Reply to message
- [x] Swipe to 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
- [x] Pagination - [ ] Pagination
- [x] Manual refresh - [x] Manual refresh
- [x] View attachments - [x] View attachments
- [x] Open photo - [x] Open photo
- [x] Internal viewer - [x] Internal viewer
- [x] External viewer - [ ] External viewer
- [ ] Open video in external player - [ ] Open video in external player
- [ ] TODO - [ ] TODO
- [ ] Caching - [ ] Caching
+4 -18
View File
@@ -1,4 +1,3 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import java.util.Properties import java.util.Properties
plugins { plugins {
@@ -13,8 +12,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "dev.meloda.fastvk" applicationId = "dev.meloda.fastvk"
versionCode = 11 versionCode = libs.versions.versionCode.get().toInt()
versionName = "0.2.3" versionName = libs.versions.versionName.get()
} }
signingConfigs { signingConfigs {
@@ -47,7 +46,7 @@ android {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
} }
named("release") { named("release") {
signingConfig = signingConfigs.getByName("debugSigning") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
@@ -59,18 +58,6 @@ android {
} }
} }
// applicationVariants.all {
// outputs.all {
// val date = System.currentTimeMillis() / 1000
// val buildType = buildType.name
// val appVersion = versionName
// val appVersionCode = versionCode
//
// val newApkName = "app-$buildType-v$appVersion($appVersionCode)-$date.apk"
// (this as? BaseVariantOutputImpl)?.outputFileName = newApkName
// }
// }
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -82,7 +69,7 @@ dependencies {
implementation(projects.feature.auth) implementation(projects.feature.auth)
implementation(projects.feature.chatmaterials) implementation(projects.feature.chatmaterials)
implementation(projects.feature.convos) implementation(projects.feature.conversations)
implementation(projects.feature.languagepicker) implementation(projects.feature.languagepicker)
implementation(projects.feature.messageshistory) implementation(projects.feature.messageshistory)
implementation(projects.feature.photoviewer) implementation(projects.feature.photoviewer)
@@ -92,7 +79,6 @@ dependencies {
implementation(projects.feature.photoviewer) implementation(projects.feature.photoviewer)
implementation(projects.feature.createchat) implementation(projects.feature.createchat)
implementation(projects.core.logger)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(projects.core.data) implementation(projects.core.data)
Binary file not shown.
+2 -9
View File
@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:tools="http://schemas.android.com/tools"> xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
@@ -38,12 +37,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="dev.meloda.fast.presentation.CrashActivity"
android:exported="false"
android:process=":error_handler"
android:theme="@style/CrashDialogTheme" />
<service <service
android:name="dev.meloda.fast.service.longpolling.LongPollingService" android:name="dev.meloda.fast.service.longpolling.LongPollingService"
android:enabled="true" android:enabled="true"
@@ -2,6 +2,7 @@ package dev.meloda.fast
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
@@ -22,52 +23,79 @@ import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings 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.logger.FastLogger
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
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainViewModel( interface MainViewModel {
val startDestination: StateFlow<Any?>
val isNeedToReplaceWithAuth: StateFlow<Boolean>
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
val isNeedToRequestNotifications: StateFlow<Boolean>
val profileImageUrl: StateFlow<String?>
fun onError(error: BaseError)
fun onNavigatedToAuth()
fun onAppResumed(intent: Intent)
@OptIn(ExperimentalPermissionsApi::class)
fun onPermissionCheckStatus(status: PermissionStatus)
fun onPermissionsRequested()
fun onNotificationsDeniedDialogConfirmClicked()
fun onNotificationsDeniedDialogCancelClicked()
fun onNotificationsDeniedDialogDismissed()
fun onNotificationsRationaleDialogDismissed()
fun onNotificationsRationaleDialogCancelClicked()
}
class MainViewModelImpl(
private val getCurrentAccountUseCase: GetCurrentAccountUseCase, private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
private val loadUserByIdUseCase: LoadUserByIdUseCase, private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val userSettings: UserSettings, private val userSettings: UserSettings,
private val longPollController: LongPollController, private val longPollController: LongPollController
private val logger: FastLogger ) : MainViewModel, ViewModel() {
) : ViewModel() {
val startDestination = MutableStateFlow<Any?>(null) override val startDestination = MutableStateFlow<Any?>(null)
val isNeedToReplaceWithAuth = MutableStateFlow(false) override val isNeedToReplaceWithAuth = MutableStateFlow(false)
val currentUser = MutableStateFlow<VkUser?>(null)
val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false) override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false) override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
val isNeedToCheckNotificationsPermission = MutableStateFlow(false) override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
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
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 is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
} }
} }
fun onNavigatedToAuth() { override fun onNavigatedToAuth() {
isNeedToReplaceWithAuth.update { false } isNeedToReplaceWithAuth.update { false }
} }
fun onAppResumed(intent: Intent) { override fun onAppResumed(intent: Intent) {
openNotificationsSettings = openNotificationsSettings =
intent.hasCategory(NotificationCompat.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) intent.hasCategory(NotificationCompat.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
openAppSettings = openAppSettings =
@@ -94,7 +122,7 @@ class MainViewModel(
} }
@ExperimentalPermissionsApi @ExperimentalPermissionsApi
fun onPermissionCheckStatus(status: PermissionStatus) { override fun onPermissionCheckStatus(status: PermissionStatus) {
isNeedToCheckNotificationsPermission.update { false } isNeedToCheckNotificationsPermission.update { false }
when (status) { when (status) {
@@ -116,46 +144,43 @@ class MainViewModel(
} }
} }
fun onPermissionsRequested() { override fun onPermissionsRequested() {
isNeedToRequestNotifications.update { false } isNeedToRequestNotifications.update { false }
} }
fun onNotificationsDeniedDialogConfirmClicked() { override fun onNotificationsDeniedDialogConfirmClicked() {
isNeedToRequestNotifications.update { true } isNeedToRequestNotifications.update { true }
} }
fun onNotificationsDeniedDialogCancelClicked() { override fun onNotificationsDeniedDialogCancelClicked() {
isNeedToShowNotificationsDeniedDialog.update { false } isNeedToShowNotificationsDeniedDialog.update { false }
disableBackgroundLongPoll() disableBackgroundLongPoll()
} }
fun onNotificationsDeniedDialogDismissed() { override fun onNotificationsDeniedDialogDismissed() {
isNeedToShowNotificationsDeniedDialog.update { false } isNeedToShowNotificationsDeniedDialog.update { false }
} }
fun onNotificationsRationaleDialogDismissed() { override fun onNotificationsRationaleDialogDismissed() {
isNeedToShowNotificationsRationaleDialog.update { false } isNeedToShowNotificationsRationaleDialog.update { false }
} }
fun onNotificationsRationaleDialogCancelClicked() { override fun onNotificationsRationaleDialogCancelClicked() {
isNeedToShowNotificationsRationaleDialog.update { false } isNeedToShowNotificationsRationaleDialog.update { false }
disableBackgroundLongPoll() disableBackgroundLongPoll()
} }
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 ->
currentUser.emit(null) profileImageUrl.emit(null)
}, },
success = { response -> success = { response ->
val user = response ?: return@listenValue val user = response ?: return@listenValue
currentUser.emit(user)
profileImageUrl.emit(user.photo100)
} }
) )
} }
@@ -171,18 +196,9 @@ class MainViewModel(
private fun loadAccounts() { private fun loadAccounts() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val currentAccount = getCurrentAccountUseCase()?.mapToDto() val currentAccount = getCurrentAccountUseCase()
logger.debug( Log.d("MainViewModel", "currentAccount: $currentAccount")
this@MainViewModel::class,
"loadAccounts(): currentAccount: %s"
.format(
currentAccount?.copy(
accessToken = if (currentAccount.accessToken.isNotEmpty()) "<redacted>"
else "null"
)
)
)
listenLongPollState() listenLongPollState()
@@ -1,25 +1,15 @@
package dev.meloda.fast.common package dev.meloda.fast.common
import android.app.Application import android.app.Application
import android.content.Intent
import android.net.Uri
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import coil.ImageLoader import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer
import dev.meloda.fast.auth.BuildConfig
import dev.meloda.fast.common.di.applicationModule import dev.meloda.fast.common.di.applicationModule
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.logger.FastLogLevel
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.presentation.CrashActivity
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.GlobalContext.startKoin import org.koin.core.context.GlobalContext.startKoin
import java.io.File
import java.io.FileOutputStream
import kotlin.system.exitProcess
class AppGlobal : Application(), ImageLoaderFactory { class AppGlobal : Application(), ImageLoaderFactory {
@@ -28,22 +18,10 @@ class AppGlobal : Application(), ImageLoaderFactory {
val preferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences = PreferenceManager.getDefaultSharedPreferences(this)
AppSettings.init(preferences) AppSettings.init(preferences)
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
initKoin() initKoin()
initCrashHandler()
val logLevel =
if (BuildConfig.DEBUG) FastLogLevel.DEBUG
else FastLogLevel.ERROR
get<FastLogger>()
.apply { setLogLevel(logLevel) }
.also { FastLogger.setInstance(it) }
} }
override fun newImageLoader(): ImageLoader = get()
private fun initKoin() { private fun initKoin() {
startKoin { startKoin {
androidLogger() androidLogger()
@@ -52,37 +30,5 @@ class AppGlobal : Application(), ImageLoaderFactory {
} }
} }
private fun initCrashHandler() { override fun newImageLoader(): ImageLoader = get()
val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
val crashLogsDirectory = File(filesDir.absolutePath + "/crashlogs")
if (!crashLogsDirectory.exists()) {
crashLogsDirectory.mkdirs()
}
val crashLogFileName = "crash_" + System.currentTimeMillis() + ".txt"
val crashLogFile = File(crashLogsDirectory.absolutePath + "/" + crashLogFileName)
FileOutputStream(crashLogFile).use { stream ->
stream.write(throwable.stackTraceToString().toByteArray())
}
if (AppSettings.Debug.showAlertAfterCrash) {
try {
val intent = Intent(this, CrashActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.putExtra("CRASH_LOG_FILE_URI", Uri.fromFile(crashLogFile))
startActivity(intent)
exitProcess(0)
} catch (e: Exception) {
if (e !is RuntimeException) {
defaultExceptionHandler?.uncaughtException(thread, throwable)
}
}
} else {
defaultExceptionHandler?.uncaughtException(thread, throwable)
}
}
}
} }
@@ -1,18 +0,0 @@
package dev.meloda.fast.common.di
import android.content.Context
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.PowerManager
import androidx.preference.PreferenceManager
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
val androidModule = module {
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
factoryOf(PreferenceManager::getDefaultSharedPreferences)
factory<Resources> { androidContext().resources }
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
factory<ConnectivityManager> { androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
}
@@ -1,42 +1,45 @@
package dev.meloda.fast.common.di package dev.meloda.fast.common.di
import android.content.Context
import android.content.res.Resources
import android.os.PowerManager
import androidx.preference.PreferenceManager
import coil.ImageLoader import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.MainViewModel import dev.meloda.fast.auth.captcha.di.captchaModule
import dev.meloda.fast.auth.authModule import dev.meloda.fast.auth.login.di.loginModule
import dev.meloda.fast.auth.validation.di.validationModule
import dev.meloda.fast.chatmaterials.di.chatMaterialsModule import dev.meloda.fast.chatmaterials.di.chatMaterialsModule
import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.LongPollControllerImpl import dev.meloda.fast.common.LongPollControllerImpl
import dev.meloda.fast.common.NetworkStateListener
import dev.meloda.fast.common.provider.Provider 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.convos.di.convosModule import dev.meloda.fast.conversations.di.conversationsModule
import dev.meloda.fast.convos.di.createChatModule 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
import dev.meloda.fast.logger.loggerModule
import dev.meloda.fast.messageshistory.di.messagesHistoryModule import dev.meloda.fast.messageshistory.di.messagesHistoryModule
import dev.meloda.fast.photoviewer.di.photoViewModule import dev.meloda.fast.photoviewer.di.photoViewModule
import dev.meloda.fast.presentation.NetworkObserver
import dev.meloda.fast.profile.di.profileModule import dev.meloda.fast.profile.di.profileModule
import dev.meloda.fast.provider.ApiLanguageProvider 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.core.module.dsl.factoryOf import org.koin.android.ext.koin.androidContext
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.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
@OptIn(ExperimentalCoilApi::class)
val applicationModule = module { val applicationModule = module {
includes(domainModule) includes(domainModule)
includes( includes(
authModule, loginModule,
convosModule, validationModule,
captchaModule,
conversationsModule,
settingsModule, settingsModule,
messagesHistoryModule, messagesHistoryModule,
photoViewModule, photoViewModule,
@@ -48,25 +51,24 @@ val applicationModule = module {
createChatModule createChatModule
) )
includes(loggerModule) // TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
includes(androidModule) // TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
singleOf(PreferenceManager::getDefaultSharedPreferences)
single<Resources> { androidContext().resources }
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
factoryOf(::ApiLanguageProvider) bind Provider::class singleOf(::ApiLanguageProvider) bind Provider::class
viewModelOf(::MainViewModel) { qualifier = qualifier("main") } viewModelOf(::MainViewModelImpl) {
qualifier = qualifier("main")
}
single<ImageLoader> { single<ImageLoader> {
ImageLoader.Builder(get()) ImageLoader.Builder(get())
.crossfade(true) .crossfade(true)
.build() .build()
.also {
it.diskCache?.directory?.toFile()?.listFiles()
}
} }
singleOf(::LongPollControllerImpl) bind LongPollController::class singleOf(::LongPollControllerImpl) bind LongPollController::class
singleOf(::ResourceProviderImpl) bind ResourceProvider::class singleOf(::ResourceProviderImpl) bind ResourceProvider::class
singleOf(::NetworkStateListener)
singleOf(::NetworkObserver) { qualifier = qualifier("main") }
} }
@@ -2,15 +2,16 @@ 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.convos.navigation.ConvoGraph import dev.meloda.fast.MainViewModel
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
import dev.meloda.fast.presentation.MainScreen import dev.meloda.fast.presentation.MainScreen
import dev.meloda.fast.profile.navigation.Profile import dev.meloda.fast.profile.navigation.Profile
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import dev.meloda.fast.ui.R as UiR
@Serializable @Serializable
object MainGraph object MainGraph
@@ -21,28 +22,29 @@ object Main
fun NavGraphBuilder.mainScreen( fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit, onSettingsButtonClicked: () -> Unit,
onNavigateToMessagesHistory: (convoId: Long) -> Unit, onConversationClicked: (conversationId: Int) -> Unit,
onPhotoClicked: (url: String) -> Unit, onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userid: Long) -> Unit, onMessageClicked: (userId: Int) -> Unit,
onNavigateToCreateChat: () -> Unit onCreateChatClicked: () -> Unit,
viewModel: MainViewModel
) { ) {
val navigationItems = ImmutableList.of( val navigationItems = ImmutableList.of(
BottomNavigationItem( BottomNavigationItem(
titleResId = R.string.title_friends, titleResId = UiR.string.title_friends,
selectedIconResId = R.drawable.ic_group_fill_round_24, selectedIconResId = UiR.drawable.baseline_people_alt_24,
unselectedIconResId = R.drawable.ic_group_round_24, unselectedIconResId = UiR.drawable.outline_people_alt_24,
route = Friends, route = Friends,
), ),
BottomNavigationItem( BottomNavigationItem(
titleResId = R.string.title_convos, titleResId = UiR.string.title_conversations,
selectedIconResId = R.drawable.ic_mail_fill_round_24, selectedIconResId = UiR.drawable.baseline_chat_24,
unselectedIconResId = R.drawable.ic_mail_round_24, unselectedIconResId = UiR.drawable.outline_chat_24,
route = ConvoGraph route = Conversations
), ),
BottomNavigationItem( BottomNavigationItem(
titleResId = R.string.title_profile, titleResId = UiR.string.title_profile,
selectedIconResId = R.drawable.ic_account_circle_fill_round_24, selectedIconResId = UiR.drawable.baseline_account_circle_24,
unselectedIconResId = R.drawable.ic_account_circle_round_24, unselectedIconResId = UiR.drawable.outline_account_circle_24,
route = Profile route = Profile
) )
) )
@@ -52,10 +54,11 @@ fun NavGraphBuilder.mainScreen(
navigationItems = navigationItems, navigationItems = navigationItems,
onError = onError, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onNavigateToMessagesHistory = onNavigateToMessagesHistory, onConversationItemClicked = onConversationClicked,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked, onMessageClicked = onMessageClicked,
onNavigateToCreateChat = onNavigateToCreateChat onCreateChatClicked = onCreateChatClicked,
viewModel = viewModel
) )
} }
} }
@@ -1,35 +0,0 @@
package dev.meloda.fast.presentation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.DialogProperties
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.MaterialDialog
@Composable
fun AppCrashedDialog(
stacktrace: String,
onDismiss: () -> Unit,
onShare: () -> Unit,
modifier: Modifier = Modifier,
) {
var showTrace by rememberSaveable { mutableStateOf(false) }
MaterialDialog(
modifier = modifier,
onDismissRequest = onDismiss,
title = stringResource(R.string.title_error),
text = if (showTrace) stacktrace else stringResource(R.string.error_occurred),
confirmText = stringResource(R.string.action_share),
confirmAction = onShare,
cancelText = stringResource(if (showTrace) R.string.action_hide_stacktrace else R.string.action_show_stacktrace),
cancelAction = { showTrace = !showTrace },
neutralText = stringResource(R.string.action_close),
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
)
}
@@ -1,71 +0,0 @@
package dev.meloda.fast.presentation
import android.content.ClipData
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.collectAsState
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.ui.theme.AppTheme
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.compose.koinInject
import java.io.File
class CrashActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val crashLogFileUri = intent.getParcelableExtra<Uri>("CRASH_LOG_FILE_URI") ?: run {
finish()
return
}
val crashLogFile = crashLogFileUri.toFile().takeIf(File::exists) ?: run {
finish()
return
}
val stacktrace = crashLogFile.bufferedReader().readText()
setContent {
val userSettings: UserSettings = koinInject()
AppTheme(
useDarkTheme = isNeedToEnableDarkMode(darkMode = userSettings.darkMode.collectAsState().value),
useDynamicColors = userSettings.enableDynamicColors.collectAsState().value,
selectedColorScheme = 0,
useAmoledBackground = userSettings.enableAmoledDark.collectAsState().value,
useSystemFont = userSettings.useSystemFont.collectAsState().value
) {
AppCrashedDialog(
stacktrace = stacktrace,
onDismiss = { finish() },
onShare = {
val uri = FileProvider.getUriForFile(
this,
"$packageName.provider",
crashLogFile
)
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
}
val chooserIntent = Intent.createChooser(sendIntent, null)
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(chooserIntent)
}
)
}
}
}
}
@@ -4,33 +4,53 @@ import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
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 com.conena.nanokt.android.content.pxToDp
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import dev.meloda.fast.MainViewModel import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.common.AppConstants import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.domain.LongPollEventsHandler import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.logger.FastLogger
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.R import dev.meloda.fast.ui.model.DeviceSize
import dev.meloda.fast.ui.common.LocalLogger import dev.meloda.fast.ui.model.SizeConfig
import org.koin.android.ext.android.get import dev.meloda.fast.ui.model.ThemeConfig
import dev.meloda.fast.ui.theme.AppTheme
import dev.meloda.fast.ui.theme.LocalSizeConfig
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.KoinContext
import org.koin.compose.koinInject import org.koin.compose.koinInject
import dev.meloda.fast.ui.R as UiR
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -67,34 +87,169 @@ class MainActivity : AppCompatActivity() {
requestNotificationPermissions() requestNotificationPermissions()
setContent { setContent {
val logger: FastLogger = koinInject() KoinContext {
val context = LocalContext.current
val userSettings: UserSettings = koinInject()
val longPollController: LongPollController = koinInject()
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val viewModel: MainViewModel = koinViewModel()
LifecycleResumeEffect(true) { LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent) viewModel.onAppResumed(intent)
onPauseOrDispose {} onPauseOrDispose {}
} }
CompositionLocalProvider(LocalLogger provides logger) { val permissionState =
RootScreen( rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
toggleLongPollService = { enable, inBackground ->
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToCheckPermission) {
if (isNeedToCheckPermission) {
viewModel.onPermissionCheckStatus(permissionState.status)
if (permissionState.status.isGranted) {
if (longPollCurrentState == LongPollState.InApp) {
toggleLongPollService(false)
}
toggleLongPollService( toggleLongPollService(
enable = enable, enable = true,
inBackground = inBackground inBackground = true
?: AppSettings.Experimental.longPollInBackground
) )
}, }
toggleOnlineService = ::toggleOnlineService }
}
LaunchedEffect(isNeedToRequestPermission) {
if (isNeedToRequestPermission) {
viewModel.onPermissionsRequested()
permissionState.launchPermissionRequest()
}
}
LaunchedEffect(longPollStateToApply) {
if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply
) {
toggleLongPollService(false)
Log.d("LongPoll", "recreate()")
}
toggleLongPollService(
enable = longPollStateToApply.isLaunched(),
inBackground = longPollStateToApply == LongPollState.Background
) )
} }
} }
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
LifecycleResumeEffect(sendOnline) {
toggleOnlineService(sendOnline)
onPauseOrDispose {
toggleOnlineService(false)
}
}
val deviceWidthDp = remember(true) {
context.resources.displayMetrics.widthPixels.pxToDp()
}
val deviceHeightDp = remember(true) {
context.resources.displayMetrics.heightPixels.pxToDp()
}
val deviceWidthSize by remember(deviceWidthDp) {
derivedStateOf {
when {
deviceWidthDp <= 360 -> DeviceSize.Small
deviceWidthDp <= 600 -> DeviceSize.Compact
deviceWidthDp <= 840 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val deviceHeightSize by remember(deviceHeightDp) {
derivedStateOf {
when {
deviceHeightDp <= 480 -> DeviceSize.Small
deviceHeightDp <= 700 -> DeviceSize.Compact
deviceHeightDp <= 900 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val sizeConfig by remember(deviceWidthSize, deviceHeightSize) {
mutableStateOf(
SizeConfig(
widthSize = deviceWidthSize,
heightSize = deviceHeightSize
)
)
}
val darkMode by userSettings.darkMode.collectAsStateWithLifecycle()
val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle()
val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle()
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
val themeConfig by remember(
darkMode,
dynamicColors,
amoledDark,
enableBlur,
enableMultiline,
setDarkMode,
useSystemFont
) {
mutableStateOf(
ThemeConfig(
darkMode = setDarkMode,
dynamicColors = dynamicColors,
selectedColorScheme = 0,
amoledDark = amoledDark,
enableBlur = enableBlur,
enableMultiline = enableMultiline,
useSystemFont = useSystemFont
)
)
}
CompositionLocalProvider(
LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig
) {
AppTheme(
useDarkTheme = themeConfig.darkMode,
useDynamicColors = themeConfig.dynamicColors,
selectedColorScheme = themeConfig.selectedColorScheme,
useAmoledBackground = themeConfig.amoledDark,
useSystemFont = themeConfig.useSystemFont
) {
RootScreen(viewModel = viewModel)
}
}
}
}
} }
private fun createNotificationChannels() { private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val noCategoryName = getString(R.string.notification_channel_no_category_name) val noCategoryName = getString(UiR.string.notification_channel_no_category_name)
val noCategoryDescriptionText = val noCategoryDescriptionText =
getString(R.string.notification_channel_no_category_description) getString(UiR.string.notification_channel_no_category_description)
val noCategoryChannel = val noCategoryChannel =
NotificationChannel( NotificationChannel(
AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED, AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED,
@@ -104,9 +259,9 @@ class MainActivity : AppCompatActivity() {
description = noCategoryDescriptionText description = noCategoryDescriptionText
} }
val longPollName = getString(R.string.notification_channel_long_polling_service_name) val longPollName = getString(UiR.string.notification_channel_long_polling_service_name)
val longPollDescriptionText = val longPollDescriptionText =
getString(R.string.notification_channel_long_polling_service_description) getString(UiR.string.notification_channel_long_polling_service_description)
val longPollChannel = val longPollChannel =
NotificationChannel( NotificationChannel(
AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING, AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING,
@@ -117,7 +272,7 @@ class MainActivity : AppCompatActivity() {
} }
val notificationManager: NotificationManager = val notificationManager: NotificationManager =
getSystemService(NOTIFICATION_SERVICE) as NotificationManager getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannels( notificationManager.createNotificationChannels(
listOf( listOf(
@@ -137,19 +292,12 @@ class MainActivity : AppCompatActivity() {
} }
} }
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.Experimental.longPollInBackground inBackground: Boolean = AppSettings.Experimental.longPollInBackground
) { ) {
if (enable) { if (enable) {
val longPollIntent = longPollingServiceIntent val longPollIntent = Intent(this, LongPollingService::class.java)
if (inBackground) { if (inBackground) {
ContextCompat.startForegroundService(this, longPollIntent) ContextCompat.startForegroundService(this, longPollIntent)
@@ -157,15 +305,15 @@ class MainActivity : AppCompatActivity() {
startService(longPollIntent) startService(longPollIntent)
} }
} else { } else {
stopService(longPollingServiceIntent) stopService(Intent(this, LongPollingService::class.java))
} }
} }
private fun toggleOnlineService(enable: Boolean) { private fun toggleOnlineService(enable: Boolean) {
if (enable) { if (enable) {
startService(onlineServiceIntent) startService(Intent(this, OnlineService::class.java))
} else { } else {
stopService(onlineServiceIntent) stopService(Intent(this, OnlineService::class.java))
} }
} }
@@ -182,7 +330,6 @@ class MainActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
stopServices() stopServices()
get<LongPollEventsHandler>().onDestroy()
} }
companion object { companion object {
@@ -1,14 +1,12 @@
package dev.meloda.fast.presentation package dev.meloda.fast.presentation
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween import androidx.compose.animation.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
@@ -27,9 +25,9 @@ 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
@@ -38,9 +36,8 @@ import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect 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.convos.model.ConvoNavigationIntent import dev.meloda.fast.MainViewModel
import dev.meloda.fast.convos.navigation.convosGraph import dev.meloda.fast.conversations.navigation.conversationsScreen
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
@@ -48,10 +45,7 @@ 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)
@@ -60,52 +54,38 @@ fun MainScreen(
navigationItems: ImmutableList<BottomNavigationItem>, navigationItems: ImmutableList<BottomNavigationItem>,
onError: (BaseError) -> Unit = {}, onError: (BaseError) -> Unit = {},
onSettingsButtonClicked: () -> Unit = {}, onSettingsButtonClicked: () -> Unit = {},
onNavigateToMessagesHistory: (convoId: Long) -> Unit = {}, onConversationItemClicked: (conversationId: Int) -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {},
onMessageClicked: (userid: Long) -> Unit = {}, onMessageClicked: (userId: Int) -> Unit = {},
onNavigateToCreateChat: () -> Unit = {} onCreateChatClicked: () -> Unit = {},
viewModel: MainViewModel
) { ) {
val activity = LocalActivity.current as? AppCompatActivity ?: return val currentTheme = LocalThemeConfig.current
val theme = LocalThemeConfig.current val hazeState = remember { HazeState() }
val hazeState = remember { HazeState(true) }
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 currentRoute = navigationItems[selectedItemIndex].route
selectedItemIndex = 1
navController.navigate(navigationItems[selectedItemIndex].route) {
popUpTo(route = currentRoute) {
inclusive = true
}
}
}
val profileImageUrl = LocalUser.current?.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 (theme.enableBlur) { if (currentTheme.enableBlur) {
Modifier.hazeEffect( Modifier.hazeEffect(
state = hazeState, state = hazeState,
style = HazeMaterials.regular(NavigationBarDefaults.containerColor) style = HazeMaterials.thick()
) )
} else Modifier } else Modifier
), )
containerColor = if (theme.enableBlur) Color.Transparent .fillMaxWidth(),
else NavigationBarDefaults.containerColor containerColor = NavigationBarDefaults.containerColor.copy(
alpha = if (currentTheme.enableBlur) 0f else 1f
)
) { ) {
navigationItems.forEachIndexed { index, item -> navigationItems.forEachIndexed { index, item ->
NavigationBarItem( NavigationBarItem(
@@ -120,10 +100,6 @@ fun MainScreen(
inclusive = true inclusive = true
} }
} }
} else {
tabReselected = tabReselected.toMutableMap().also {
it[navigationItems[index].route] = true
}
} }
}, },
icon = { icon = {
@@ -147,7 +123,9 @@ 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 = { isLoading = false } onSuccess = {
isLoading = false
}
) )
} else { } else {
Icon( Icon(
@@ -168,12 +146,11 @@ 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 padding.calculateBottomPadding(), LocalBottomPadding provides if (currentTheme.enableBlur) padding.calculateBottomPadding() else 0.dp
LocalReselectedTab provides tabReselected,
LocalNavController provides navController
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
@@ -187,33 +164,23 @@ fun MainScreen(
exitTransition = { fadeOut(animationSpec = tween(200)) } exitTransition = { fadeOut(animationSpec = tween(200)) }
) { ) {
friendsScreen( friendsScreen(
activity = activity,
onError = onError, onError = onError,
navController = navController,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked, onMessageClicked = onMessageClicked
onScrolledToTop = {
tabReselected = tabReselected.toMutableMap().also {
it[Friends] = false
}
},
) )
convosGraph( conversationsScreen(
handleNavigationIntent = { intent -> onError = onError,
when (intent) { onConversationItemClicked = onConversationItemClicked,
ConvoNavigationIntent.Back -> {} onPhotoClicked = onPhotoClicked,
ConvoNavigationIntent.Archive -> {} onCreateChatClicked = onCreateChatClicked,
ConvoNavigationIntent.CreateChat -> onNavigateToCreateChat() navController = navController,
is ConvoNavigationIntent.MessagesHistory -> {
onNavigateToMessagesHistory(intent.convoId)
}
}
},
activity = activity,
) )
profileScreen( profileScreen(
activity = activity, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onPhotoClicked = onPhotoClicked onPhotoClicked = onPhotoClicked,
navController = navController
) )
} }
} }
@@ -1,238 +0,0 @@
package dev.meloda.fast.presentation
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import dev.meloda.fast.common.NetworkStateListener
import dev.meloda.fast.common.model.NetworkState
import dev.meloda.fast.common.model.NetworkStatus
import dev.meloda.fast.common.model.NetworkType
import dev.meloda.fast.logger.FastLogger
import java.util.concurrent.ConcurrentHashMap
internal class NetworkObserver(
private val connectivityManager: ConnectivityManager,
private val logger: FastLogger,
private val networkStateListener: NetworkStateListener
) {
private val networks = ConcurrentHashMap<Network, NetworkModel>()
private var clearCallbacks: (() -> Unit)? = null
init {
startListener()
}
private fun syncNetworkState() {
val state = if (networks.values.any { it.isInternetAvailable() }) {
NetworkState.CONNECTED
} else {
NetworkState.DISCONNECTED
}
networkStateListener.updateNetworkState(state)
log("STATE: $state")
}
private fun startListener() {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
log("onAvailable(): network: $network")
networks[network] = mapNetworkModel(
network = network,
capabilities = connectivityManager.getNetworkCapabilities(network),
properties = connectivityManager.getLinkProperties(network),
status = NetworkStatus.AVAILABLE,
assumeInternet = true
)
syncNetworkState()
}
override fun onUnavailable() {
log("onUnavailable()")
networks.clear()
syncNetworkState()
}
override fun onLost(network: Network) {
log("onLost() network: $network")
networks.remove(network)
syncNetworkState()
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
log("onCapabilitiesChanged(): network: $network; caps: $networkCapabilities")
val current = networks[network]
networks[network] = mapNetworkModel(
network = network,
capabilities = networkCapabilities,
from = current,
status = current?.status ?: NetworkStatus.AVAILABLE
)
syncNetworkState()
}
override fun onBlockedStatusChanged(
network: Network,
blocked: Boolean
) {
log("onBlockedStatusChanged(): network: $network; blocked: $blocked")
networks[network] = mapNetworkModel(
network = network,
from = networks[network],
status = if (blocked) NetworkStatus.BLOCKED else NetworkStatus.UNBLOCKED
)
syncNetworkState()
}
override fun onLinkPropertiesChanged(
network: Network,
linkProperties: LinkProperties
) {
log("onLinkPropertiesChanged(): network: $network; props: $linkProperties")
val current = networks[network]
networks[network] = mapNetworkModel(
network = network,
properties = linkProperties,
from = current,
status = current?.status ?: NetworkStatus.AVAILABLE
)
syncNetworkState()
}
override fun onLosing(network: Network, maxMsToLive: Int) {
log("onLosing(): network: $network; maxMsToLive: $maxMsToLive")
val current = networks[network]
networks[network] = mapNetworkModel(
network = network,
maxMsToLive = maxMsToLive.toLong(),
from = current,
status = current?.status ?: NetworkStatus.AVAILABLE
)
syncNetworkState()
}
override fun onReserved(networkCapabilities: NetworkCapabilities) {
log("onReserved(): caps: $networkCapabilities")
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
clearCallbacks = { connectivityManager.unregisterNetworkCallback(callback) }
refreshActiveNetwork()
}
private fun log(text: String) {
logger.debug(this::class, text)
}
private fun refreshActiveNetwork() {
val network = connectivityManager.activeNetwork
if (network == null) {
networks.clear()
syncNetworkState()
return
}
val capabilities = connectivityManager.getNetworkCapabilities(network)
if (capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true) {
networks[network] = mapNetworkModel(
network = network,
capabilities = capabilities,
properties = connectivityManager.getLinkProperties(network),
status = NetworkStatus.AVAILABLE
)
}
syncNetworkState()
}
private fun mapNetworkModel(
network: Network,
capabilities: NetworkCapabilities? = null,
properties: LinkProperties? = null,
status: NetworkStatus? = null,
maxMsToLive: Long? = null,
from: NetworkModel? = null,
assumeInternet: Boolean = false
): NetworkModel {
val caps = capabilities
?: from?.networkCapabilities
?: connectivityManager.getNetworkCapabilities(network)
val networkType = when {
caps?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> NetworkType.CELLULAR
caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> NetworkType.WIFI
else -> from?.type ?: NetworkType.UNKNOWN
}
val hasInternet = if (caps != null) {
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else {
from?.hasInternet ?: assumeInternet
}
val signalStrength =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
caps?.signalStrength
} else {
null
} ?: from?.signalStrength ?: Int.MAX_VALUE
return NetworkModel(
id = network.hashCode(),
type = networkType,
original = network,
hasInternet = hasInternet,
signalStrength = signalStrength,
status = status ?: from?.status ?: NetworkStatus.UNAVAILABLE,
maxMsToLive = maxMsToLive ?: from?.maxMsToLive,
networkCapabilities = caps,
linkProperties = properties
?: from?.linkProperties
?: connectivityManager.getLinkProperties(network)
)
}
}
data class NetworkModel(
val id: Int,
val type: NetworkType,
val original: Network,
val hasInternet: Boolean,
val signalStrength: Int,
val status: NetworkStatus,
val maxMsToLive: Long?,
val networkCapabilities: NetworkCapabilities?,
val linkProperties: LinkProperties?
) {
fun isStatusOk(): Boolean = status.isOk()
fun isInternetAvailable(): Boolean = hasInternet && isStatusOk()
}
@@ -1,248 +1,49 @@
package dev.meloda.fast.presentation package dev.meloda.fast.presentation
import android.Manifest
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.activity.compose.LocalActivity
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.fillMaxSize
import androidx.compose.material3.AlertDialog 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.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.conena.nanokt.android.content.pxToDp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import dev.meloda.fast.MainViewModel import dev.meloda.fast.MainViewModel
import dev.meloda.fast.auth.authNavGraph import dev.meloda.fast.auth.authNavGraph
import dev.meloda.fast.auth.captcha.presentation.CaptchaScreen
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.common.LongPollController import dev.meloda.fast.conversations.navigation.createChatScreen
import dev.meloda.fast.common.NetworkStateListener import dev.meloda.fast.conversations.navigation.navigateToCreateChat
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.convos.navigation.createChatScreen
import dev.meloda.fast.convos.navigation.navigateToCreateChat
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.CaptchaTokenResult
import dev.meloda.fast.datastore.UserSettings
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
import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.navigation.Main import dev.meloda.fast.navigation.Main
import dev.meloda.fast.navigation.mainScreen import dev.meloda.fast.navigation.mainScreen
import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog import dev.meloda.fast.photoviewer.navigation.navigateToPhotoView
import dev.meloda.fast.settings.model.SettingsNavigationIntent 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.common.LocalLogger
import dev.meloda.fast.ui.common.LocalNetworkState
import dev.meloda.fast.ui.common.LocalSizeConfig
import dev.meloda.fast.ui.model.DeviceSize
import dev.meloda.fast.ui.model.SizeConfig
import dev.meloda.fast.ui.model.ThemeConfig
import dev.meloda.fast.ui.theme.AppTheme
import dev.meloda.fast.ui.theme.LocalNavController
import dev.meloda.fast.ui.theme.LocalNavRootController
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun RootScreen( fun RootScreen(
toggleLongPollService: (enable: Boolean, inBackground: Boolean?) -> Unit, navController: NavHostController = rememberNavController(),
toggleOnlineService: (enable: Boolean) -> Unit viewModel: MainViewModel
) { ) {
val logger = LocalLogger.current
val resources = LocalResources.current
val userSettings: UserSettings = koinInject()
val longPollController: LongPollController = koinInject()
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel()
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
val permissionState =
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToCheckPermission) {
if (isNeedToCheckPermission) {
viewModel.onPermissionCheckStatus(permissionState.status)
if (permissionState.status.isGranted) {
if (longPollCurrentState == LongPollState.InApp) {
toggleLongPollService(false, null)
}
toggleLongPollService(true, true)
}
}
}
LaunchedEffect(isNeedToRequestPermission) {
if (isNeedToRequestPermission) {
viewModel.onPermissionsRequested()
permissionState.launchPermissionRequest()
}
}
LifecycleResumeEffect(longPollStateToApply) {
logger.debug("RootScreen", "longPollStateToApply: $longPollStateToApply")
if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply
) {
toggleLongPollService(false, null)
}
toggleLongPollService(
longPollStateToApply.isLaunched(),
longPollStateToApply == LongPollState.Background
)
}
onPauseOrDispose {}
}
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
LifecycleResumeEffect(sendOnline) {
toggleOnlineService(sendOnline)
onPauseOrDispose {
toggleOnlineService(false)
}
}
val deviceWidthDp = remember(true) {
resources.displayMetrics.widthPixels.pxToDp()
}
val deviceHeightDp = remember(true) {
resources.displayMetrics.heightPixels.pxToDp()
}
val deviceWidthSize by remember(deviceWidthDp) {
derivedStateOf {
when {
deviceWidthDp <= 360 -> DeviceSize.Small
deviceWidthDp <= 600 -> DeviceSize.Compact
deviceWidthDp <= 840 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val deviceHeightSize by remember(deviceHeightDp) {
derivedStateOf {
when {
deviceHeightDp <= 480 -> DeviceSize.Small
deviceHeightDp <= 700 -> DeviceSize.Compact
deviceHeightDp <= 900 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val sizeConfig by remember(deviceWidthSize, deviceHeightSize) {
mutableStateOf(
SizeConfig(
widthSize = deviceWidthSize,
heightSize = deviceHeightSize
)
)
}
val darkMode by userSettings.darkMode.collectAsStateWithLifecycle()
val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle()
val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle()
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
val themeConfig by remember(
darkMode,
dynamicColors,
amoledDark,
enableBlur,
enableMultiline,
setDarkMode,
useSystemFont
) {
derivedStateOf {
ThemeConfig(
darkMode = setDarkMode,
dynamicColors = dynamicColors,
selectedColorScheme = 0,
amoledDark = amoledDark,
enableBlur = enableBlur,
enableMultiline = enableMultiline,
useSystemFont = useSystemFont,
enableAnimations = enableAnimations
)
}
}
val networkStateListener: NetworkStateListener = koinInject()
val networkState by networkStateListener.networkStateFlow.collectAsStateWithLifecycle()
LaunchedEffect(networkState) {
logger.debug("RootScreen", "NetworkState: $networkState")
}
CompositionLocalProvider(
LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig,
LocalUser provides currentUser,
LocalNetworkState provides networkState
) {
AppTheme(
useDarkTheme = themeConfig.darkMode,
useDynamicColors = themeConfig.dynamicColors,
selectedColorScheme = themeConfig.selectedColorScheme,
useAmoledBackground = themeConfig.amoledDark,
useSystemFont = themeConfig.useSystemFont
) {
val navController: NavHostController = rememberNavController()
val activity = LocalActivity.current
val context = LocalContext.current val context = LocalContext.current
val startDestination by viewModel.startDestination.collectAsStateWithLifecycle() val startDestination by viewModel.startDestination.collectAsStateWithLifecycle()
val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle() val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle()
@@ -310,18 +111,6 @@ fun RootScreen(
} }
if (startDestination != null) { if (startDestination != null) {
CompositionLocalProvider(
LocalNavRootController provides navController,
LocalNavController provides navController
) {
var photoViewerInfo by rememberSaveable {
mutableStateOf<Pair<List<String>, Int?>?>(null)
}
val captchaRedirectUri by AppSettings.getCaptchaRedirectUriFlow()
.collectAsStateWithLifecycle()
Box(modifier = Modifier.fillMaxSize()) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = requireNotNull(startDestination), startDestination = requireNotNull(startDestination),
@@ -329,97 +118,44 @@ fun RootScreen(
exitTransition = { fadeOut(animationSpec = tween(200)) } exitTransition = { fadeOut(animationSpec = tween(200)) }
) { ) {
authNavGraph( authNavGraph(
onNavigateToMain = { onNavigateToMain = navController::navigateToMain,
viewModel.onUserAuthenticated()
navController.navigateToMain()
},
onNavigateToSettings = navController::navigateToSettings,
navController = navController navController = navController
) )
mainScreen( mainScreen(
onError = viewModel::onError, onError = viewModel::onError,
onSettingsButtonClicked = navController::navigateToSettings, onSettingsButtonClicked = navController::navigateToSettings,
onNavigateToMessagesHistory = navController::navigateToMessagesHistory, onConversationClicked = navController::navigateToMessagesHistory,
onPhotoClicked = { url -> onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
photoViewerInfo = listOf(url) to null
},
onMessageClicked = navController::navigateToMessagesHistory, onMessageClicked = navController::navigateToMessagesHistory,
onNavigateToCreateChat = navController::navigateToCreateChat, onCreateChatClicked = navController::navigateToCreateChat,
viewModel = viewModel
) )
messagesHistoryScreen( messagesHistoryScreen(
onError = viewModel::onError, onError = viewModel::onError,
onBack = navController::navigateUp, onBack = navController::navigateUp,
onNavigateToChatMaterials = navController::navigateToChatMaterials, onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials
onNavigateToPhotoViewer = { photos, index ->
photoViewerInfo = photos to index
}
) )
chatMaterialsScreen( chatMaterialsScreen(
onBack = navController::navigateUp, onBack = navController::navigateUp,
onPhotoClicked = { url -> onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
photoViewerInfo = listOf(url) to null
}
) )
createChatScreen( createChatScreen(
onChatCreated = { convoId -> onChatCreated = { conversationId ->
navController.popBackStack() navController.popBackStack()
navController.navigateToMessagesHistory(convoId) navController.navigateToMessagesHistory(conversationId)
}, },
navController = navController navController = navController
) )
settingsScreen( settingsScreen(
handleNavigationIntent = { intent -> onBack = navController::navigateUp,
when (intent) { onLogOutButtonClicked = { navController.navigateToAuth(true) },
SettingsNavigationIntent.Back -> navController.navigateUp() onLanguageItemClicked = navController::navigateToLanguagePicker
SettingsNavigationIntent.Language -> navController.navigateToLanguagePicker()
SettingsNavigationIntent.Restart -> {
activity?.let {
val intent =
Intent(activity, MainActivity::class.java)
intent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP
) )
activity.finish()
activity.startActivity(intent)
}
}
SettingsNavigationIntent.LogOut -> {
navController.navigateToAuth(true)
}
}
}
)
languagePickerScreen(onBack = navController::navigateUp) languagePickerScreen(onBack = navController::navigateUp)
}
PhotoViewDialog( photoViewScreen(onBack = navController::navigateUp)
photoViewerInfo = photoViewerInfo?.let { info ->
info.first.toImmutableList() to info.second
},
onDismiss = { photoViewerInfo = null }
)
CaptchaScreen(
captchaRedirectUri = captchaRedirectUri,
onBack = {
AppSettings.setCaptchaResult(CaptchaTokenResult.Cancelled)
},
onResult = { result ->
AppSettings.setCaptchaResult(
CaptchaTokenResult.Success(result)
)
},
)
}
}
}
} }
} }
} }
@@ -6,9 +6,8 @@ import android.os.IBinder
import android.util.Log import android.util.Log
import dev.meloda.fast.common.extensions.createTimerFlow import dev.meloda.fast.common.extensions.createTimerFlow
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.processState
import dev.meloda.fast.domain.AccountUseCase import dev.meloda.fast.domain.AccountUseCase
import dev.meloda.fast.logger.FastLogger import dev.meloda.fast.data.processState
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -25,12 +24,11 @@ import kotlin.time.Duration.Companion.minutes
class OnlineService : Service() { class OnlineService : Service() {
private val logger: FastLogger by inject()
private val job = SupervisorJob() private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
logger.error(this::class.java, "CoroutineException", throwable) Log.d(TAG, "error: $throwable")
throwable.printStackTrace()
} }
private val coroutineContext: CoroutineContext private val coroutineContext: CoroutineContext
@@ -44,20 +42,17 @@ class OnlineService : Service() {
private var onlineJob: Job? = null private var onlineJob: Job? = null
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
logger.debug(this::class, "STATE: onBind(): intent: $intent") Log.d(STATE_TAG, "onBind: intent: $intent")
return null return null
} }
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
logger.debug( Log.d(STATE_TAG, "onStartCommand: flags: $flags; startId: $startId\ninstance: $this")
this::class,
"STATE: onStartCommand(): flags: %s; startId: %s;\ninstance: %s"
.format("$flags", "$startId", "$this")
)
createTimer() createTimer()
return START_STICKY return START_STICKY
} }
@@ -73,13 +68,13 @@ class OnlineService : Service() {
private fun setOnline() { private fun setOnline() {
if (onlineJob != null) return if (onlineJob != null) return
logger.debug(this::class, "setOnline()") Log.d(TAG, "setOnline()")
onlineJob = coroutineScope.launch { onlineJob = coroutineScope.launch {
val token = UserConfig.fastToken ?: UserConfig.accessToken val token = UserConfig.fastToken ?: UserConfig.accessToken
if (token.isBlank()) { if (token.isBlank()) {
logger.debug(this::class, "setOnline(): token is empty") Log.d(TAG, "setOnline: token is empty")
return@launch return@launch
} }
@@ -89,10 +84,10 @@ class OnlineService : Service() {
).onEach { state -> ).onEach { state ->
state.processState( state.processState(
error = { error -> error = { error ->
logger.error(this@OnlineService::class, "setOnline(): ERROR: $error") Log.w(TAG, "setOnline(): error: $error")
}, },
success = { response -> success = { response ->
logger.debug(this@OnlineService::class, "setOnline(): response: $response") Log.d(TAG, "setOnline(): success: $response")
} }
) )
}.collect() }.collect()
@@ -101,7 +96,7 @@ class OnlineService : Service() {
} }
override fun onDestroy() { override fun onDestroy() {
logger.debug(this::class, "onDestroy()") Log.d(STATE_TAG, "onDestroy")
timerJob?.cancel("OnlineService destroyed") timerJob?.cancel("OnlineService destroyed")
onlineJob?.cancel("OnlineService destroyed") onlineJob?.cancel("OnlineService destroyed")
@@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.provider.Settings import android.provider.Settings
import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import com.conena.nanokt.android.app.stopForegroundCompat import com.conena.nanokt.android.app.stopForegroundCompat
@@ -18,10 +19,8 @@ import dev.meloda.fast.common.model.LongPollState
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.LongPollEventsHandler
import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.domain.LongPollUseCase
import dev.meloda.fast.logger.FastLogger
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
@@ -31,25 +30,27 @@ 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 kotlinx.coroutines.suspendCancellableCoroutine
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.time.Duration.Companion.seconds import kotlin.coroutines.suspendCoroutine
class LongPollingService : Service() { class LongPollingService : Service() {
private val logger: FastLogger by inject()
private val longPollController: LongPollController by inject() private val longPollController: LongPollController by inject()
private val job = SupervisorJob() private val job = SupervisorJob()
private val exceptionHandler = private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
CoroutineExceptionHandler { _, throwable -> Log.e(TAG, "error: $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
@@ -59,32 +60,37 @@ class LongPollingService : Service() {
private val longPollUseCase: LongPollUseCase by inject() private val longPollUseCase: LongPollUseCase by inject()
private val updatesParser: LongPollUpdatesParser by inject() private val updatesParser: LongPollUpdatesParser by inject()
private val eventsHandler: LongPollEventsHandler by inject()
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()
logger.debug(this::class, "STATE: onCreate()") Log.d(STATE_TAG, "onCreate()")
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
logger.debug(this::class, "STATE: onBind(): intent: $intent") Log.d(STATE_TAG, "onBind: intent: $intent")
return null return null
} }
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
logger.debug( val inBackground = AppSettings.Experimental.longPollInBackground
this::class,
"STATE: onStartCommand(): asForeground: %s; flags: %s; startId: %s;\ninstance: %s" Log.d(
.format("$inBackground", "$flags", "$startId", "$this") STATE_TAG,
"onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this"
) )
startJob() if (currentJob != null) {
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)
@@ -102,6 +108,11 @@ 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(
@@ -123,37 +134,17 @@ 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) {
logger.debug( Log.d(STATE_TAG, "job is completed or cancelled")
this::class,
"startPolling(): Job is already done. isCompleted: %s; isCancelled: %s"
.format("${job.isCompleted}", "${job.isCancelled}")
)
throw Exception("Job is over") throw Exception("Job is over")
} }
logger.debug(this::class, "startPolling(): Starting job.") Log.d(STATE_TAG, "job started")
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()
@@ -202,7 +193,7 @@ class LongPollingService : Service() {
if (updates == null) { if (updates == null) {
failCount++ failCount++
} else { } else {
parseUpdates(updates) updates.forEach(updatesParser::parseNextUpdate)
} }
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
@@ -213,18 +204,18 @@ class LongPollingService : Service() {
} }
} }
private suspend fun getServerInfo(): VkLongPollData? = suspendCancellableCoroutine { private suspend fun getServerInfo(): VkLongPollData? = suspendCoroutine {
longPollUseCase.getLongPollServer( longPollUseCase.getLongPollServer(
needPts = true, needPts = true,
version = VkConstants.LP_VERSION version = VkConstants.LP_VERSION
).listenValue(coroutineScope) { state -> ).listenValue(coroutineScope) { state ->
state.processState( state.processState(
success = { response -> success = { response ->
logger.debug(this::class, "getServerInfo(): response: $response") Log.d(TAG, "getServerInfo: serverInfoResponse: $response")
it.resume(response) it.resume(response)
}, },
error = { error -> error = { error ->
logger.error(this::class, "getServerInfo(): ERROR: $error") Log.e(TAG, "getServerInfo: $error")
it.resume(null) it.resume(null)
} }
) )
@@ -233,7 +224,7 @@ class LongPollingService : Service() {
private suspend fun getUpdatesResponse( private suspend fun getUpdatesResponse(
server: VkLongPollData server: VkLongPollData
): LongPollUpdates? = suspendCancellableCoroutine { ): LongPollUpdates? = suspendCoroutine {
longPollUseCase.getLongPollUpdates( longPollUseCase.getLongPollUpdates(
serverUrl = "https://${server.server}", serverUrl = "https://${server.server}",
key = server.key, key = server.key,
@@ -244,40 +235,21 @@ class LongPollingService : Service() {
).listenValue(coroutineScope) { state -> ).listenValue(coroutineScope) { state ->
state.processState( state.processState(
success = { response -> success = { response ->
logger.debug(this::class, "getUpdatesResponse(): response: $response") Log.d(TAG, "lastUpdateResponse: $response")
it.resume(response) it.resume(response)
}, },
error = { error -> error = { error ->
logger.debug(this::class, "getUpdatesResponse(): error: $error") Log.d(TAG, "getUpdatesResponse: error: $error")
it.resume(null) it.resume(null)
} }
) )
} }
} }
private suspend fun parseUpdates(updates: List<List<Any>>) {
val parsedUpdates = updates.flatMap { updatesParser.parseNextUpdate(it) }
eventsHandler.handleEvents(parsedUpdates)
}
private fun handleError(throwable: Throwable) {
logger.error(this::class, "CoroutineException", throwable)
if (throwable !is NoAccessTokenException) {
throwable.printStackTrace()
}
coroutineScope.launch {
delay(5.seconds)
startJob()
}
longPollController.updateCurrentState(LongPollState.Exception)
}
override fun onDestroy() { override fun onDestroy() {
logger.debug(this::class, "STATE: onDestroy()") Log.d(STATE_TAG, "onDestroy")
longPollController.updateCurrentState(LongPollState.Stopped) longPollController.updateCurrentState(LongPollState.Stopped)
updatesParser.clearListeners()
try { try {
AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) } AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) }
job.cancel() job.cancel()
@@ -288,7 +260,7 @@ class LongPollingService : Service() {
} }
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {
logger.debug(this::class, "STATE: onTrimMemory(): Level: $level") Log.d(STATE_TAG, "onTrimMemory. Level: $level")
super.onTrimMemory(level) super.onTrimMemory(level)
} }
@@ -304,4 +276,4 @@ class LongPollingService : Service() {
} }
private data class LongPollException(override val message: String) : Throwable() private data class LongPollException(override val message: String) : Throwable()
private class NoAccessTokenException : Throwable() private data object NoAccessTokenException : Throwable()
@@ -1,6 +1,5 @@
package dev.meloda.fast.service.longpolling.di package dev.meloda.fast.service.longpolling.di
import dev.meloda.fast.domain.LongPollEventsHandler
import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.domain.LongPollUseCase
import dev.meloda.fast.domain.LongPollUseCaseImpl import dev.meloda.fast.domain.LongPollUseCaseImpl
@@ -11,5 +10,4 @@ import org.koin.dsl.module
val longPollModule = module { val longPollModule = module {
singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class
singleOf(::LongPollUpdatesParser) singleOf(::LongPollUpdatesParser)
singleOf(::LongPollEventsHandler)
} }
@@ -6,7 +6,7 @@ import android.content.Context
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import dev.meloda.fast.common.AppConstants import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R as UiR
object NotificationsUtils { object NotificationsUtils {
@@ -28,7 +28,7 @@ object NotificationsUtils {
actions: List<NotificationCompat.Action> = emptyList(), actions: List<NotificationCompat.Action> = emptyList(),
): NotificationCompat.Builder { ): NotificationCompat.Builder {
val builder = NotificationCompat.Builder(context, channelId) val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_fast_logo) .setSmallIcon(UiR.drawable.ic_fast_logo)
.setContentTitle(title) .setContentTitle(title)
.setPriority(priority.value) .setPriority(priority.value)
.setContentIntent(contentIntent) .setContentIntent(contentIntent)
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools"> <network-security-config xmlns:tools="http://schemas.android.com/tools">
<!-- Allow cleartext network traffic -->
<base-config <base-config
cleartextTrafficPermitted="false" cleartextTrafficPermitted="true"
tools:ignore="InsecureBaseConfiguration"> tools:ignore="InsecureBaseConfiguration">
<trust-anchors> <trust-anchors>
<!-- Trust pre-installed CAs --> <!-- Trust pre-installed CAs -->
+3 -3
View File
@@ -7,13 +7,13 @@ plugins {
group = "dev.meloda.fast.buildlogic" group = "dev.meloda.fast.buildlogic"
java { java {
sourceCompatibility = JavaVersion.VERSION_21 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_17
} }
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget = JvmTarget.JVM_21 jvmTarget = JvmTarget.JVM_17
} }
} }
@@ -10,7 +10,6 @@ class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
with(target) { with(target) {
apply(plugin = "com.android.application") apply(plugin = "com.android.application")
apply(plugin = "org.jetbrains.kotlin.plugin.compose") apply(plugin = "org.jetbrains.kotlin.plugin.compose")
apply(plugin = "com.github.skydoves.compose.stability.analyzer")
val extension = extensions.getByType<ApplicationExtension>() val extension = extensions.getByType<ApplicationExtension>()
configureAndroidCompose(extension) configureAndroidCompose(extension)
@@ -1,6 +1,6 @@
import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationExtension
import dev.meloda.fast.configureKotlinAndroid import dev.meloda.fast.configureKotlinAndroid
import dev.meloda.fast.getVersionInt import dev.meloda.fast.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
@@ -10,18 +10,12 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
with(target) { with(target) {
with(pluginManager) { with(pluginManager) {
apply("com.android.application") apply("com.android.application")
apply("org.jetbrains.kotlin.android")
} }
extensions.configure<ApplicationExtension> { extensions.configure<ApplicationExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig { defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
minSdk = getVersionInt("minSdk")
compileSdk = getVersionInt("compileSdk")
targetSdk = getVersionInt("targetSdk")
}
lint {
abortOnError = false
}
} }
} }
} }
@@ -1,26 +1,18 @@
import com.android.build.api.dsl.LibraryExtension import com.android.build.gradle.LibraryExtension
import dev.meloda.fast.configureAndroidCompose import dev.meloda.fast.configureAndroidCompose
import dev.meloda.fast.getVersionInt
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.getByType
class AndroidLibraryComposeConventionPlugin : Plugin<Project> { class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
apply(plugin = "com.android.library") apply(plugin = "com.android.library")
apply(plugin = "org.jetbrains.kotlin.plugin.compose") apply(plugin = "org.jetbrains.kotlin.plugin.compose")
apply(plugin = "com.github.skydoves.compose.stability.analyzer")
extensions.configure<LibraryExtension> { val extension = extensions.getByType<LibraryExtension>()
configureAndroidCompose(this) configureAndroidCompose(extension)
androidResources.enable = false
defaultConfig {
minSdk = getVersionInt("minSdk")
compileSdk = getVersionInt("compileSdk")
}
}
} }
} }
} }
@@ -1,8 +1,8 @@
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.LibraryExtension
import dev.meloda.fast.configureKotlinAndroid import dev.meloda.fast.configureKotlinAndroid
import dev.meloda.fast.disableUnnecessaryAndroidTests import dev.meloda.fast.disableUnnecessaryAndroidTests
import dev.meloda.fast.getVersionInt import dev.meloda.fast.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
@@ -14,20 +14,15 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
with(target) { with(target) {
with(pluginManager) { with(pluginManager) {
apply("com.android.library") apply("com.android.library")
apply("org.jetbrains.kotlin.android")
apply("org.jetbrains.kotlin.plugin.parcelize") apply("org.jetbrains.kotlin.plugin.parcelize")
apply("org.jetbrains.kotlin.plugin.serialization") apply("org.jetbrains.kotlin.plugin.serialization")
} }
extensions.configure<LibraryExtension> { extensions.configure<LibraryExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
androidResources.enable = false defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
defaultConfig { defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
minSdk = getVersionInt("minSdk")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
lint {
abortOnError = false
}
} }
extensions.configure<LibraryAndroidComponentsExtension> { extensions.configure<LibraryAndroidComponentsExtension> {
disableUnnecessaryAndroidTests(target) disableUnnecessaryAndroidTests(target)
@@ -1,6 +1,6 @@
import com.android.build.api.dsl.TestExtension import com.android.build.gradle.TestExtension
import dev.meloda.fast.configureKotlinAndroid import dev.meloda.fast.configureKotlinAndroid
import dev.meloda.fast.getVersionInt import dev.meloda.fast.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
@@ -10,15 +10,12 @@ class AndroidTestConventionPlugin : Plugin<Project> {
with(target) { with(target) {
with(pluginManager) { with(pluginManager) {
apply("com.android.test") apply("com.android.test")
apply("org.jetbrains.kotlin.android")
} }
extensions.configure<TestExtension> { extensions.configure<TestExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig { defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
minSdk = getVersionInt("minSdk")
compileSdk = getVersionInt("compileSdk")
targetSdk = getVersionInt("targetSdk")
}
} }
} }
} }
@@ -5,10 +5,12 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
internal fun Project.configureAndroidCompose( internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension, commonExtension: CommonExtension<*, *, *, *, *, *>,
) { ) {
commonExtension.apply { commonExtension.apply {
buildFeatures.compose = true buildFeatures {
compose = true
}
dependencies { dependencies {
val bom = libs.findLibrary("compose-bom").get() val bom = libs.findLibrary("compose-bom").get()
@@ -1,9 +1,6 @@
package dev.meloda.fast package dev.meloda.fast
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.CommonExtension
import com.android.build.api.dsl.CompileOptions
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.JavaVersion import org.gradle.api.JavaVersion
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.plugins.JavaPluginExtension
@@ -16,29 +13,28 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
internal fun Project.configureKotlinAndroid( internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension, commonExtension: CommonExtension<*, *, *, *, *, *>,
) { ) {
when (commonExtension) { commonExtension.apply {
is ApplicationExtension -> commonExtension.compileOptions(buildCompileOptions()) compileSdk = libs.findVersion("compileSdk").get().toString().toInt()
is LibraryExtension -> commonExtension.compileOptions(buildCompileOptions())
defaultConfig {
minSdk = libs.findVersion("minSdk").get().toString().toInt()
} }
commonExtension.apply { compileOptions {
compileSdk = getVersionInt("compileSdk") sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
} }
configureKotlin<KotlinAndroidProjectExtension>() configureKotlin<KotlinAndroidProjectExtension>()
} }
private fun buildCompileOptions(): CompileOptions.() -> Unit = {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
internal fun Project.configureKotlinJvm() { internal fun Project.configureKotlinJvm() {
extensions.configure<JavaPluginExtension> { extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_21 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_17
} }
configureKotlin<KotlinJvmProjectExtension>() configureKotlin<KotlinJvmProjectExtension>()
@@ -51,17 +47,15 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() =
when (this) { when (this) {
is KotlinAndroidProjectExtension -> compilerOptions is KotlinAndroidProjectExtension -> compilerOptions
is KotlinJvmProjectExtension -> compilerOptions is KotlinJvmProjectExtension -> compilerOptions
else -> throw IllegalArgumentException("Unsupported project extension $this ${T::class}") else -> TODO("Unsupported project extension $this ${T::class}")
}.apply { }.apply {
jvmTarget = JvmTarget.JVM_21 jvmTarget = JvmTarget.JVM_17
allWarningsAsErrors = warningsAsErrors.toBoolean() allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs.addAll( freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn", "-opt-in=kotlin.RequiresOptIn",
// Enable experimental coroutines APIs, including Flow // Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview"
"-Xannotation-default-target=param-property",
"-Xcontext-parameters"
) )
} }
} }
@@ -7,8 +7,3 @@ import org.gradle.kotlin.dsl.getByType
val Project.libs val Project.libs
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs") get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
fun Project.getVersionInt(alias: String): Int {
return libs.findVersion(alias).get().requiredVersion.toInt()
}
+1 -2
View File
@@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.jvm) apply false
@@ -8,6 +9,4 @@ plugins {
alias(libs.plugins.room) apply false alias(libs.plugins.room) apply false
alias(libs.plugins.ksp) apply false alias(libs.plugins.ksp) apply false
alias(libs.plugins.module.graph) apply true alias(libs.plugins.module.graph) apply true
alias(libs.plugins.versions) apply true
alias(libs.plugins.stability.analyzer) apply false
} }
@@ -4,9 +4,9 @@ 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.263" const val API_VERSION = "5.173"
const val URL_OAUTH = "https://oauth.vk.ru" const val URL_OAUTH = "https://oauth.vk.com"
const val URL_API = "https://api.vk.ru/method" const val URL_API = "https://api.vk.com/method"
const val NOTIFICATION_CHANNEL_UNCATEGORIZED = "uncategorized" const val NOTIFICATION_CHANNEL_UNCATEGORIZED = "uncategorized"
const val NOTIFICATION_CHANNEL_LONG_POLLING = "long_polling" const val NOTIFICATION_CHANNEL_LONG_POLLING = "long_polling"
@@ -1,23 +0,0 @@
package dev.meloda.fast.common
import dev.meloda.fast.common.model.NetworkState
import dev.meloda.fast.common.model.NetworkStatus
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class NetworkStateListener {
private val networkStatus = MutableStateFlow(NetworkStatus.UNAVAILABLE)
val networkStatusFlow = networkStatus.asStateFlow()
private val networkState = MutableStateFlow(NetworkState.DISCONNECTED)
val networkStateFlow = networkState.asStateFlow()
fun updateNetworkState(state: NetworkState) {
networkState.value = state
networkStatus.value = when (state) {
NetworkState.CONNECTED -> NetworkStatus.AVAILABLE
NetworkState.DISCONNECTED -> NetworkStatus.UNAVAILABLE
}
}
}
@@ -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_info,last_seen,verified,sex,bdate" "photo_50,photo_100,photo_200,photo_400_orig,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info,bdate"
const val ALL_FIELDS = const val ALL_FIELDS =
"$USER_FIELDS,$GROUP_FIELDS" "$USER_FIELDS,$GROUP_FIELDS"
const val LP_VERSION = 19 const val LP_VERSION = 10
const val VK_APP_ID = "2274003" const val VK_APP_ID = "2274003"
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
@@ -18,11 +18,6 @@ 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,14 +1,9 @@
package dev.meloda.fast.common.extensions package dev.meloda.fast.common.extensions
import android.os.Build
import android.os.Bundle
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
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -16,8 +11,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.reflect.KClass
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -30,30 +23,6 @@ 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
}
context(viewModel: ViewModel)
fun <T> Flow<T>.listenValue(action: suspend (T) -> Unit): Job =
listenValue(viewModel.viewModelScope, action)
context(viewModel: ViewModel)
fun <T> MutableSharedFlow<T>.emitOnMain(value: T) {
val flow = this
viewModel.viewModelScope.launch { flow.emit(value) }
}
fun <T> Flow<T>.listenValue( fun <T> Flow<T>.listenValue(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
action: suspend (T) -> Unit action: suspend (T) -> Unit
@@ -106,11 +75,6 @@ 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()
@@ -119,14 +83,6 @@ 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) }
@@ -134,33 +90,3 @@ fun <T> Any.toList(mapper: (old: Any) -> T): List<T> {
else -> emptyList() else -> emptyList()
} }
} }
fun <T> Bundle.getParcelableCompat(key: String, clazz: Class<T>): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelable(key, clazz)
} else {
@Suppress("DEPRECATION")
getParcelable(key)
}
}
fun <T : Any> Bundle.getParcelableCompat(key: String, clazz: KClass<T>): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelable(key, clazz.java)
} else {
@Suppress("DEPRECATION")
getParcelable(key)
}
}
infix fun ClosedRange<Int>.collidesWith(other: ClosedRange<Int>): Boolean {
return this.start < other.endInclusive && other.start < this.endInclusive
}
operator fun ClosedRange<Int>.minus(other: ClosedRange<Int>): ClosedRange<Int> {
return (this.start - other.start)..(this.endInclusive - other.endInclusive)
}
operator fun ClosedRange<Int>.minus(other: Int): ClosedRange<Int> {
return (this.start - other)..(this.endInclusive - other)
}
@@ -1,13 +1,13 @@
package dev.meloda.fast.common.model package dev.meloda.fast.common.model
enum class NetworkLogLevel(val value: Int) { enum class LogLevel(val value: Int) {
NONE(0), NONE(0),
BASIC(1), BASIC(1),
HEADERS(2), HEADERS(2),
BODY(3); BODY(3);
companion object { companion object {
fun parse(value: Int): NetworkLogLevel = entries.firstOrNull { it.value == value } fun parse(value: Int): LogLevel = entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown log level with value: $value") ?: throw IllegalArgumentException("Unknown log level with value: $value")
} }
} }
@@ -1,3 +0,0 @@
package dev.meloda.fast.common.model
enum class NetworkState { CONNECTED, DISCONNECTED }
@@ -1,10 +0,0 @@
package dev.meloda.fast.common.model
enum class NetworkStatus {
AVAILABLE, UNAVAILABLE, LOST, BLOCKED, UNBLOCKED;
fun isOk(): Boolean = when (this) {
AVAILABLE, UNBLOCKED -> true
UNAVAILABLE, LOST, BLOCKED -> false
}
}
@@ -1,5 +0,0 @@
package dev.meloda.fast.common.model
enum class NetworkType {
CELLULAR, WIFI, UNKNOWN
}
@@ -3,7 +3,9 @@ package dev.meloda.fast.common.provider
import android.content.res.Resources import android.content.res.Resources
interface ResourceProvider { interface ResourceProvider {
val resources: Resources val resources: Resources
fun getString(resId: Int): String fun getString(resId: Int): String
} }
@@ -1,22 +1,19 @@
package dev.meloda.fast.common.util package dev.meloda.fast.common.util
import android.content.res.Resources
import com.conena.nanokt.jvm.util.dayOfMonth import com.conena.nanokt.jvm.util.dayOfMonth
import com.conena.nanokt.jvm.util.hour
import com.conena.nanokt.jvm.util.hourOfDay import com.conena.nanokt.jvm.util.hourOfDay
import com.conena.nanokt.jvm.util.millisecond import com.conena.nanokt.jvm.util.millisecond
import com.conena.nanokt.jvm.util.minute import com.conena.nanokt.jvm.util.minute
import com.conena.nanokt.jvm.util.month import com.conena.nanokt.jvm.util.month
import com.conena.nanokt.jvm.util.second import com.conena.nanokt.jvm.util.second
import com.conena.nanokt.jvm.util.year import com.conena.nanokt.jvm.util.year
import dev.meloda.fast.common.R
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Instant
object TimeUtils { object TimeUtils {
@@ -30,11 +27,7 @@ object TimeUtils {
}.timeInMillis }.timeInMillis
} }
fun getLocalizedDate( fun getLocalizedDate(resources: Resources, date: Long): String {
date: Long,
yesterday: () -> String,
today: () -> String
): String {
val now = Calendar.getInstance() val now = Calendar.getInstance()
val then = Calendar.getInstance().also { it.timeInMillis = date } val then = Calendar.getInstance().also { it.timeInMillis = date }
@@ -43,41 +36,48 @@ object TimeUtils {
now.month != then.month -> "dd MMMM" now.month != then.month -> "dd MMMM"
now.dayOfMonth != then.dayOfMonth -> { now.dayOfMonth != then.dayOfMonth -> {
if (now.dayOfMonth - then.dayOfMonth == 1) { if (now.dayOfMonth - then.dayOfMonth == 1) {
return yesterday() return resources.getString(R.string.yesterday)
} else { } else {
"dd MMMM" "dd MMMM"
} }
} }
else -> return today() else -> return resources.getString(R.string.today)
} }
return SimpleDateFormat(pattern, Locale.getDefault()).format(date) return SimpleDateFormat(pattern, Locale.getDefault()).format(date)
} }
fun getLocalizedTime( fun getLocalizedTime(resources: Resources, date: Long): String {
date: Long, val now = Calendar.getInstance()
yearShort: () -> String, val then = Calendar.getInstance().also { it.timeInMillis = date }
monthShort: () -> String,
weekShort: () -> String,
dayShort: () -> String,
minuteShort: () -> String,
secondShort: () -> String,
now: () -> String
): String {
val now = Clock.System.now()
val then = Instant.fromEpochMilliseconds(date)
val diff = now - then
return when { return when {
diff > 365.days -> "${diff.inWholeDays / 365}${yearShort().lowercase()}" now.year != then.year -> {
diff > 30.days -> "${diff.inWholeDays / 30}${monthShort().lowercase()}" "${now.year - then.year}${resources.getString(R.string.year_short).lowercase()}"
diff > 7.days -> "${diff.inWholeDays / 7}${weekShort().lowercase()}" }
diff > 1.days -> "${diff.inWholeDays}${dayShort().lowercase()}"
diff > 1.hours -> "${diff.inWholeHours}h" now.month != then.month -> {
diff > 1.minutes -> "${diff.inWholeMinutes}${minuteShort().lowercase()}" "${now.month - then.month}${resources.getString(R.string.month_short).lowercase()}"
diff > 1.seconds -> "${diff.inWholeSeconds}${secondShort().lowercase()}" }
else -> now().lowercase()
now.dayOfMonth != then.dayOfMonth -> {
val change = now.dayOfMonth - then.dayOfMonth
if (change % 7 == 0) {
"${change / 7}${resources.getString(R.string.week_short).lowercase()}"
} else {
"$change${resources.getString(R.string.day_short).lowercase()}"
}
}
now.hour == then.hour && now.minute == then.minute -> {
resources.getString(R.string.time_now).lowercase()
}
else -> {
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
}
} }
} }
} }
@@ -1,17 +0,0 @@
package dev.meloda.fast.common.util
import java.net.URLEncoder
import java.security.MessageDigest
fun String.urlEncode(encoding: String = "utf-8"): String {
return URLEncoder.encode(this, encoding)
}
fun String.sha256() = this.hashString("SHA-256")
fun String.hashString(algorithm: String): String {
return MessageDigest
.getInstance(algorithm)
.digest(this.toByteArray())
.fold("") { str, it -> str + "%02x".format(it) }
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="yesterday">Вчера</string>
<string name="today">Сегодня</string>
<string name="year_short">Г</string>
<string name="month_short">М</string>
<string name="week_short">Н</string>
<string name="day_short">Д</string>
<string name="time_now">Сейчас</string>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="yesterday">Yesterday</string>
<string name="today">Today</string>
<string name="year_short">Y</string>
<string name="month_short">M</string>
<string name="week_short">W</string>
<string name="day_short">D</string>
<string name="time_now">Now</string>
</resources>
+1
View File
@@ -14,6 +14,7 @@ dependencies {
api(projects.core.network) api(projects.core.network)
api(projects.core.database) api(projects.core.database)
// TODO: 11/08/2024, Danil Nikolaev: remove?
implementation(libs.retrofit) implementation(libs.retrofit)
implementation(libs.eithernet) implementation(libs.eithernet)
implementation(libs.koin.android) implementation(libs.koin.android)
@@ -25,6 +25,8 @@ sealed class State<out T> {
data object InternalError : Error() data object InternalError : Error()
data class OAuthError(val error: OAuthErrorDomain) : Error() data class OAuthError(val error: OAuthErrorDomain) : Error()
data class TestError(val message: String) : Error()
} }
fun isLoading(): Boolean = this is Loading fun isLoading(): Boolean = this is Loading
@@ -36,16 +38,16 @@ sealed class State<out T> {
} }
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 -> {
any()
error(this) error(this)
any()
} }
State.Idle -> idle() State.Idle -> idle()
@@ -53,47 +55,17 @@ inline fun <T> State<T>.processState(
State.Loading -> loading() State.Loading -> loading()
is State.Success -> { is State.Success -> {
any()
success(data) success(data)
any()
} }
} }
} }
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)
@@ -6,18 +6,17 @@ object UserConfig {
private const val ARG_CURRENT_USER_ID = "current_user_id" private const val ARG_CURRENT_USER_ID = "current_user_id"
var currentUserId: Long = -1 var currentUserId: Int = -1
get() = AppSettings.getLong(ARG_CURRENT_USER_ID, -1) get() = AppSettings.getInt(ARG_CURRENT_USER_ID, -1)
set(value) { set(value) {
field = value field = value
AppSettings.edit { putLong(ARG_CURRENT_USER_ID, value) } AppSettings.edit { putInt(ARG_CURRENT_USER_ID, value) }
} }
var userId: Long = -1 var userId: Int = -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
@@ -1,7 +1,7 @@
package dev.meloda.fast.data package dev.meloda.fast.data
import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.domain.VkConvo import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkGroupDomain
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import kotlin.math.abs import kotlin.math.abs
@@ -10,15 +10,15 @@ class VkGroupsMap(
private val groups: List<VkGroupDomain> private val groups: List<VkGroupDomain>
) { ) {
private val map: HashMap<Long, VkGroupDomain> by lazy { private val map: HashMap<Int, VkGroupDomain> by lazy {
HashMap(groups.associateBy(VkGroupDomain::id)) HashMap(groups.associateBy(VkGroupDomain::id))
} }
fun groups(): List<VkGroupDomain> = map.values.toList() fun groups(): List<VkGroupDomain> = map.values.toList()
fun convoGroup(convo: VkConvo): VkGroupDomain? = fun conversationGroup(conversation: VkConversation): VkGroupDomain? =
if (!convo.peerType.isGroup()) null if (!conversation.peerType.isGroup()) null
else map[abs(convo.id)] else map[abs(conversation.id)]
fun messageActionGroup(message: VkMessage): VkGroupDomain? = fun messageActionGroup(message: VkMessage): VkGroupDomain? =
if (message.actionMemberId == null || message.actionMemberId!! >= 0) null if (message.actionMemberId == null || message.actionMemberId!! >= 0) null
@@ -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: Long): VkGroupDomain? = map[abs(groupId)] fun group(groupId: Int): VkGroupDomain? = map[abs(groupId)]
companion object { companion object {
@@ -1,8 +1,7 @@
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.VkConvo import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkGroupDomain 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.VkUser
@@ -10,11 +9,11 @@ import kotlin.math.abs
object VkMemoryCache { object VkMemoryCache {
private val users: HashMap<Long, VkUser> = hashMapOf() private val users: HashMap<Int, VkUser> = hashMapOf()
private val groups: HashMap<Long, VkGroupDomain> = hashMapOf() private val groups: HashMap<Int, VkGroupDomain> = hashMapOf()
private val messages: HashMap<Long, VkMessage> = hashMapOf() private val messages: HashMap<Int, VkMessage> = hashMapOf()
private val convos: HashMap<Long, VkConvo> = hashMapOf() private val conversations: HashMap<Int, VkConversation> = hashMapOf()
private val contacts: HashMap<Long, VkContactDomain> = hashMapOf() private val contacts: HashMap<Int, 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 }
@@ -28,93 +27,93 @@ object VkMemoryCache {
messages.forEach { message -> VkMemoryCache.messages[message.id] = message } messages.forEach { message -> VkMemoryCache.messages[message.id] = message }
} }
fun appendConvos(convos: List<VkConvo>) { fun appendConversations(conversations: List<VkConversation>) {
convos.forEach { convo -> conversations.forEach { conversation ->
VkMemoryCache.convos[convo.id] = convo VkMemoryCache.conversations[conversation.id] = conversation
} }
} }
fun appendContacts(contacts: List<VkContactDomain>) { fun appendContacts(contacts: List<VkContactDomain>) {
contacts.forEach { contact -> VkMemoryCache[contact.userId] = contact } contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact }
} }
operator fun set(userid: Long, user: VkUser) { operator fun set(userId: Int, user: VkUser) {
users[userId] = user users[userId] = user
} }
operator fun set(groupId: Long, group: VkGroupDomain) { operator fun set(groupId: Int, group: VkGroupDomain) {
groups[groupId] = group groups[groupId] = group
} }
operator fun set(messageId: Long, message: VkMessage) { operator fun set(messageId: Int, message: VkMessage) {
messages[messageId] = message messages[messageId] = message
} }
operator fun set(convoId: Long, convo: VkConvo) { operator fun set(conversationId: Int, conversation: VkConversation) {
convos[convoId] = convo conversations[conversationId] = conversation
} }
operator fun set(contactId: Long, contact: VkContactDomain) { operator fun set(contactId: Int, contact: VkContactDomain) {
contacts[contactId] = contact contacts[contactId] = contact
} }
fun getUser(id: Long): VkUser? { fun getUser(id: Int): VkUser? {
return getUsers(id).firstOrNull() return getUsers(id).firstOrNull()
} }
fun getUsers(vararg ids: Long): List<VkUser> { fun getUsers(vararg ids: Int): List<VkUser> {
return getUsers(ids.toList()) return getUsers(ids.toList())
} }
fun getUsers(ids: List<Long>): List<VkUser> { fun getUsers(ids: List<Int>): List<VkUser> {
return ids.mapNotNull { id -> users[id] } return ids.mapNotNull { id -> users[id] }
} }
fun getGroup(id: Long): VkGroupDomain? { fun getGroup(id: Int): VkGroupDomain? {
return getGroups(id).firstOrNull() return getGroups(id).firstOrNull()
} }
fun getGroups(vararg ids: Long): List<VkGroupDomain> { fun getGroups(vararg ids: Int): List<VkGroupDomain> {
return getGroups(ids.toList()) return getGroups(ids.toList())
} }
fun getGroups(ids: List<Long>): List<VkGroupDomain> { fun getGroups(ids: List<Int>): List<VkGroupDomain> {
return ids.mapNotNull { id -> groups[id] } return ids.mapNotNull { id -> groups[id] }
} }
fun getMessage(id: Long): VkMessage? { fun getMessage(id: Int): VkMessage? {
return getMessages(id).firstOrNull() return getMessages(id).firstOrNull()
} }
fun getMessages(vararg ids: Long): List<VkMessage> { fun getMessages(vararg ids: Int): List<VkMessage> {
return getMessages(ids.toList()) return getMessages(ids.toList())
} }
fun getMessages(ids: List<Long>): List<VkMessage> { fun getMessages(ids: List<Int>): List<VkMessage> {
return ids.mapNotNull { id -> messages[id] } return ids.mapNotNull { id -> messages[id] }
} }
fun getConvo(id: Long): VkConvo? { fun getConversation(id: Int): VkConversation? {
return getConvos(id).firstOrNull() return getConversations(id).firstOrNull()
} }
fun getConvos(vararg ids: Long): List<VkConvo> { fun getConversations(vararg ids: Int): List<VkConversation> {
return getConvos(ids.toList()) return getConversations(ids.toList())
} }
fun getConvos(ids: List<Long>): List<VkConvo> { fun getConversations(ids: List<Int>): List<VkConversation> {
return ids.mapNotNull { id -> convos[id] } return ids.mapNotNull { id -> conversations[id] }
} }
fun getContact(id: Long): VkContactDomain? { fun getContact(id: Int): VkContactDomain? {
return getContacts(id).firstOrNull() return getContacts(id).firstOrNull()
} }
fun getContacts(vararg ids: Long): List<VkContactDomain> { fun getContacts(vararg ids: Int): List<VkContactDomain> {
return getContacts(ids.toList()) return getContacts(ids.toList())
} }
fun getContacts(ids: List<Long>): List<VkContactDomain> { fun getContacts(ids: List<Int>): List<VkContactDomain> {
return ids.mapNotNull { id -> contacts[id] } return ids.mapNotNull { id -> contacts[id] }
} }
} }
@@ -1,7 +1,7 @@
package dev.meloda.fast.data package dev.meloda.fast.data
import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.domain.VkConvo import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
@@ -9,15 +9,15 @@ class VkUsersMap(
private val users: List<VkUser> private val users: List<VkUser>
) { ) {
private val map: HashMap<Long, VkUser> by lazy { private val map: HashMap<Int, VkUser> by lazy {
HashMap(users.associateBy(VkUser::id)) HashMap(users.associateBy(VkUser::id))
} }
fun users(): List<VkUser> = map.values.toList() fun users(): List<VkUser> = map.values.toList()
fun convoUser(convo: VkConvo): VkUser? = fun conversationUser(conversation: VkConversation): VkUser? =
if (!convo.peerType.isUser()) null if (!conversation.peerType.isUser()) null
else map[convo.id] else map[conversation.id]
fun messageActionUser(message: VkMessage): VkUser? = fun messageActionUser(message: VkMessage): VkUser? =
if (message.actionMemberId == null || message.actionMemberId!! <= 0) null if (message.actionMemberId == null || message.actionMemberId!! <= 0) null
@@ -35,7 +35,7 @@ class VkUsersMap(
if (message.fromId > 0) map[message.fromId] if (message.fromId > 0) map[message.fromId]
else null else null
fun user(userId: Long): VkUser? = map[userId] fun user(userId: Int): VkUser? = map[userId]
companion object { companion object {
@@ -1,34 +0,0 @@
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,32 +1,12 @@
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,17 +1,10 @@
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
@@ -19,50 +12,9 @@ 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()
}
} }
@@ -0,0 +1,22 @@
package dev.meloda.fast.data.api.conversations
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.network.RestApiErrorDomain
interface ConversationsRepository {
suspend fun getConversations(
count: Int?,
offset: Int?
): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun getConversationsById(
peerIds: List<Int>
): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun storeConversations(conversations: List<VkConversation>)
suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain>
suspend fun pin(peerId: Int): ApiResult<Int, RestApiErrorDomain>
suspend fun unpin(peerId: Int): ApiResult<Int, RestApiErrorDomain>
}
@@ -0,0 +1,148 @@
package dev.meloda.fast.data.api.conversations
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap
import dev.meloda.fast.database.dao.ConversationDao
import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.data.asDomain
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.asEntity
import dev.meloda.fast.model.api.requests.ConversationsDeleteRequest
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.mapApiDefault
import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.conversations.ConversationsService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ConversationsRepositoryImpl(
private val conversationsService: ConversationsService,
private val conversationDao: ConversationDao
) : ConversationsRepository {
override suspend fun getConversations(
count: Int?,
offset: Int?
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsGetRequest(
count = count,
offset = offset,
fields = VkConstants.ALL_FIELDS,
filter = "all",
extended = true,
startMessageId = null
)
conversationsService.getConversations(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
response.items.map { item ->
val lastMessage = item.lastMessage?.asDomain()?.let { message ->
message.copy(
user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message)
).also { VkMemoryCache[message.id] = it }
}
item.conversation.asDomain(lastMessage).let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
}
}
},
errorMapper = { error ->
error?.toDomain()
}
)
}
override suspend fun getConversationsById(
peerIds: List<Int>
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestParams = mapOf(
"peer_ids" to peerIds.joinToString(separator = ","),
"extended" to "1",
"fields" to VkConstants.ALL_FIELDS
)
conversationsService.getConversationsById(requestParams).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
response.items.map { item ->
item.asDomain().let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
}
}
},
errorMapper = { error ->
error?.toDomain()
}
)
}
override suspend fun storeConversations(conversations: List<VkConversation>) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
}
override suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
val requestModel = ConversationsDeleteRequest(peerId = peerId)
conversationsService.delete(requestModel.map).mapApiResult(
successMapper = { response -> response.requireResponse().lastDeletedId },
errorMapper = { error -> error?.toDomain() }
)
}
override suspend fun pin(
peerId: Int
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsPinRequest(peerId = peerId)
conversationsService.pin(requestModel.map).mapApiDefault()
}
override suspend fun unpin(
peerId: Int
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsUnpinRequest(peerId = peerId)
conversationsService.unpin(requestModel.map).mapApiDefault()
}
}
@@ -1,30 +0,0 @@
package dev.meloda.fast.data.api.convos
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.network.RestApiErrorDomain
interface ConvosRepository {
suspend fun storeConvos(convos: List<VkConvo>)
suspend fun getConvos(
count: Int?,
offset: Int?,
filter: ConvosFilter
): ApiResult<List<VkConvo>, RestApiErrorDomain>
suspend fun getConvosById(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): ApiResult<List<VkConvo>, 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,201 +0,0 @@
package dev.meloda.fast.data.api.convos
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap
import dev.meloda.fast.database.dao.ConvoDao
import dev.meloda.fast.database.dao.GroupDao
import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.data.asDomain
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkGroupDomain
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.model.api.domain.asEntity
import dev.meloda.fast.model.api.requests.ConvosGetRequest
import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.convos.ConvosService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ConvosRepositoryImpl(
private val convosService: ConvosService,
private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val convoDao: ConvoDao
) : ConvosRepository {
override suspend fun storeConvos(convos: List<VkConvo>) {
convoDao.insertAll(convos.map(VkConvo::asEntity))
}
override suspend fun getConvos(
count: Int?,
offset: Int?,
filter: ConvosFilter
): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConvosGetRequest(
count = count,
offset = offset,
fields = VkConstants.ALL_FIELDS,
filter = filter,
extended = true,
startMessageId = null
)
convosService.getConvos(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
val convos = response.items.map { item ->
val lastMessage = item.lastMessage?.asDomain()?.let { message ->
message.copy(
user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message),
replyMessage = message.replyMessage?.copy(
user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message),
)
).also { VkMemoryCache[message.id] = it }
}
item.convo.asDomain(lastMessage).let { convo ->
convo.copy(
user = usersMap.convoUser(convo),
group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[convo.id] = it }
}
}
val messages = convos.mapNotNull(VkConvo::lastMessage)
launch(Dispatchers.IO) {
convoDao.insertAll(convos.map(VkConvo::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
convos
},
errorMapper = { error ->
error?.toDomain()
}
)
}
override suspend fun getConvosById(
peerIds: List<Long>,
extended: Boolean?,
fields: String?
): ApiResult<List<VkConvo>, 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 }
}
convosService.getConvosById(requestParams).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
val convos = response.items.map { item ->
item.asDomain().let { convo ->
convo.copy(
user = usersMap.convoUser(convo),
group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[convo.id] = it }
}
}
launch(Dispatchers.IO) {
convoDao.insertAll(convos.map(VkConvo::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
convos
},
errorMapper = { error ->
error?.toDomain()
}
)
}
override suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
convosService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult(
successMapper = { response -> response.requireResponse().lastDeletedId },
errorMapper = { error -> error?.toDomain() }
)
}
override suspend fun pin(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
override suspend fun unpin(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
override suspend fun reorderPinned(
peerIds: List<Long>
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService
.reorderPinned(mapOf("peer_ids" to peerIds.joinToString(",")))
.mapApiDefault()
}
override suspend fun archive(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
override suspend fun unarchive(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
}
@@ -16,7 +16,7 @@ class FilesRepository(
// AUDIO_MESSAGE("audio_message") // AUDIO_MESSAGE("audio_message")
// } // }
// //
// suspend fun getMessagesUploadServer(peerid: Long, type: FileType) = // suspend fun getMessagesUploadServer(peerId: Int, type: FileType) =
// filesService.getUploadServer( // filesService.getUploadServer(
// mapOf( // mapOf(
// "peer_id" to peerId.toString(), // "peer_id" to peerId.toString(),
@@ -8,13 +8,11 @@ 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>
@@ -22,7 +20,7 @@ interface FriendsRepository {
suspend fun getOnlineFriends( suspend fun getOnlineFriends(
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<List<Long>, RestApiErrorDomain> ): ApiResult<List<Int>, RestApiErrorDomain>
suspend fun storeUsers(users: List<VkUser>) suspend fun storeUsers(users: List<VkUser>)
} }
@@ -1,12 +1,9 @@
package dev.meloda.fast.data.api.friends package dev.meloda.fast.data.api.friends
import com.slack.eithernet.ApiResult
import com.slack.eithernet.successOrElse
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.UserDao import dev.meloda.fast.database.dao.UsersDao
import dev.meloda.fast.model.FriendsInfo import dev.meloda.fast.model.FriendsInfo
import dev.meloda.fast.model.api.data.VkContactData
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
@@ -16,21 +13,22 @@ 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.friends.FriendsService import dev.meloda.fast.network.service.friends.FriendsService
import com.slack.eithernet.ApiResult
import com.slack.eithernet.successOrElse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class FriendsRepositoryImpl( class FriendsRepositoryImpl(
private val service: FriendsService, private val service: FriendsService,
private val dao: UserDao private val dao: UsersDao
) : 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(order, count, offset) }.await() val friends = async { getFriends(count, offset) }.await()
.successOrElse { failure -> .successOrElse { failure ->
return@withContext failure return@withContext failure
} }
@@ -44,26 +42,21 @@ 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 = order, order = "hints",
count = count, count = count,
offset = offset, offset = offset,
fields = VkConstants.USER_FIELDS, fields = VkConstants.USER_FIELDS
extended = true
) )
service.getFriends(requestModel.map).mapApiResult( service.getFriends(requestModel.map).mapApiResult(
successMapper = { apiResponse -> successMapper = { apiResponse ->
val response = apiResponse.requireResponse() val response = apiResponse.requireResponse()
val users = response.items.map(VkUserData::mapToDomain) val users = response.items.map(VkUserData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
VkMemoryCache.appendUsers(users) VkMemoryCache.appendUsers(users)
VkMemoryCache.appendContacts(contactsList)
users users
}, },
@@ -74,7 +67,7 @@ class FriendsRepositoryImpl(
override suspend fun getOnlineFriends( override suspend fun getOnlineFriends(
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<List<Long>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = GetOnlineFriendsRequest( val requestModel = GetOnlineFriendsRequest(
order = "hints", order = "hints",
count = count, count = count,
@@ -1,9 +1,9 @@
package dev.meloda.fast.data.api.messages package dev.meloda.fast.data.api.messages
import dev.meloda.fast.model.api.domain.VkConvo import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
data class MessagesHistoryInfo( data class MessagesHistoryInfo(
val messages: List<VkMessage>, val messages: List<VkMessage>,
val convos: List<VkConvo> val conversations: List<VkConversation>
) )
@@ -1,119 +1,82 @@
package dev.meloda.fast.data.api.messages package dev.meloda.fast.data.api.messages
import com.slack.eithernet.ApiResult 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.MessagesGetConvoMembersResponse
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
interface MessagesRepository { interface MessagesRepository {
suspend fun storeMessages(messages: List<VkMessage>)
suspend fun getHistory( suspend fun getHistory(
convoId: Long, conversationId: Int,
offset: Int?, offset: Int?,
count: Int? count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> ): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
suspend fun getById( suspend fun getById(
peerCmIds: List<Long>?, messagesIds: List<Int>,
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: Long, peerId: Int,
randomId: Long, randomId: Int,
message: String?, message: String?,
forward: String?, replyTo: Int?,
attachments: List<VkAttachment>?, attachments: List<VkAttachment>?
formatData: VkMessage.FormatData? ): ApiResult<Int, RestApiErrorDomain>
): ApiResult<MessagesSendResponse, RestApiErrorDomain>
suspend fun markAsRead( suspend fun markAsRead(
peerId: Long, peerId: Int,
startMessageId: Long? startMessageId: Int?
): ApiResult<Int, RestApiErrorDomain> ): ApiResult<Int, RestApiErrorDomain>
suspend fun getHistoryAttachments( suspend fun getHistoryAttachments(
peerId: Long, peerId: Int,
count: Int?, count: Int?,
offset: Int?, offset: Int?,
attachmentTypes: List<String>, attachmentTypes: List<String>,
cmId: Long conversationMessageId: Int
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> ): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain>
suspend fun createChat( suspend fun createChat(
userIds: List<Long>?, userIds: List<Int>?,
title: String? title: String?
): ApiResult<Long, RestApiErrorDomain>
suspend fun pin(
peerId: Long,
messageId: Long? = null,
cmId: Long? = null
): ApiResult<VkMessage, RestApiErrorDomain>
suspend fun unpin(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> ): ApiResult<Int, RestApiErrorDomain>
suspend fun markAsImportant( suspend fun storeMessages(messages: List<VkMessage>)
peerId: Long,
messageIds: List<Long>? = null,
cmIds: List<Long>? = null,
important: Boolean
): ApiResult<List<Long>, RestApiErrorDomain>
suspend fun delete( // suspend fun markAsImportant(
peerId: Long, // params: MessagesMarkAsImportantRequest
messageIds: List<Long>?, // ): ApiResult<List<Int>, RestApiErrorDomain>
cmIds: List<Long>?, //
spam: Boolean, // suspend fun pin(
deleteForAll: Boolean // params: MessagesPinMessageRequest
): ApiResult<List<Any>, RestApiErrorDomain> // ): ApiResult<VkMessageData, RestApiErrorDomain>
//
suspend fun edit( // suspend fun unpin(
peerId: Long, // params: MessagesUnPinMessageRequest
messageId: Long? = null, // ): ApiResult<Unit, RestApiErrorDomain>
cmId: Long? = null, //
message: String? = null, // suspend fun delete(
lat: Float? = null, // params: MessagesDeleteRequest
long: Float? = null, // ): ApiResult<Unit, RestApiErrorDomain>
attachments: List<VkAttachment>? = null, //
notParseLinks: Boolean = false, // suspend fun edit(
keepSnippets: Boolean = true, // params: MessagesEditRequest
keepForwardedMessages: Boolean = true // ): ApiResult<Int, RestApiErrorDomain>
): ApiResult<Int, RestApiErrorDomain> //
// suspend fun getChat(
suspend fun getChat( // params: MessagesGetChatRequest
chatId: Long, // ): ApiResult<VkChatData, RestApiErrorDomain>
fields: String? = null //
): ApiResult<VkChatData, RestApiErrorDomain> // suspend fun getConversationMembers(
// params: MessagesGetConversationMembersRequest
suspend fun getConvoMembers( // ): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain>
peerId: Long, //
offset: Int? = null, // suspend fun removeChatUser(
count: Int? = null, // params: MessagesRemoveChatUserRequest
extended: Boolean? = null, // ): ApiResult<Int, RestApiErrorDomain>
fields: String? = null
): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain>
suspend fun removeChatUser(
chatId: Long,
memberId: Long
): ApiResult<Int, RestApiErrorDomain>
suspend fun getMessageReadPeers(
peerId: Long,
cmId: Long
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain>
} }
@@ -5,65 +5,43 @@ 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.ConvoDao
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.VkConvo
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.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.MessagesGetConvoMembersRequest
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.MessagesGetConvoMembersResponse
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.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 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 convoDao: ConvoDao
) : MessagesRepository { ) : MessagesRepository {
override suspend fun getHistory( override suspend fun getHistory(
convoId: Long, conversationId: Int,
offset: Int?, offset: Int?,
count: Int? count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryRequest( val requestModel = MessagesGetHistoryRequest(
count = count, count = count,
offset = offset, offset = offset,
peerId = convoId, peerId = conversationId,
extended = true, extended = true,
startMessageId = null, startMessageId = null,
rev = null, rev = null,
@@ -91,40 +69,25 @@ class MessagesRepositoryImpl(
user = usersMap.messageUser(message), user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message), group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message), actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message), actionGroup = groupsMap.messageActionGroup(message)
replyMessage = message.replyMessage.let { replyMessage ->
replyMessage?.copy(
user = usersMap.messageUser(replyMessage),
group = groupsMap.messageGroup(replyMessage),
actionUser = usersMap.messageActionUser(replyMessage),
actionGroup = groupsMap.messageActionGroup(replyMessage),
)
}
).also { VkMemoryCache[message.id] = it } ).also { VkMemoryCache[message.id] = it }
} }
} }
val convos = response.convos.orEmpty().map { item -> val conversations = response.conversations.orEmpty().map { item ->
val message = messages.firstOrNull { it.id == item.lastMessageId } val message = messages.firstOrNull { it.id == item.lastMessageId }
item.asDomain(message) item.asDomain(message)
.let { convo -> .let { conversation ->
convo.copy( conversation.copy(
user = usersMap.convoUser(convo), user = usersMap.conversationUser(conversation),
group = groupsMap.convoGroup(convo) group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[convo.id] = it } ).also { VkMemoryCache[conversation.id] = it }
} }
} }
launch(Dispatchers.IO) {
convoDao.insertAll(convos.map(VkConvo::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,
convos = convos conversations = conversations
) )
}, },
errorMapper = { error -> errorMapper = { error ->
@@ -134,18 +97,12 @@ class MessagesRepositoryImpl(
} }
override suspend fun getById( override suspend fun getById(
peerCmIds: List<Long>?, messagesIds: List<Int>,
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
) )
@@ -155,70 +112,45 @@ 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))
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) messages.map { message ->
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),
actionUser = usersMap.messageActionUser(message), actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message), actionGroup = groupsMap.messageActionGroup(message)
replyMessage = message.replyMessage?.asDomain().let { replyMessage ->
replyMessage?.copy(
user = usersMap.messageUser(replyMessage),
group = groupsMap.messageGroup(replyMessage),
actionUser = usersMap.messageActionUser(replyMessage),
actionGroup = groupsMap.messageActionGroup(replyMessage),
) )
} }
)
}
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: Long, peerId: Int,
randomId: Long, randomId: Int,
message: String?, message: String?,
forward: String?, replyTo: Int?,
attachments: List<VkAttachment>?, attachments: List<VkAttachment>?
formatData: VkMessage.FormatData? ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesSendRequest( val requestModel = MessagesSendRequest(
peerId = peerId, peerId = peerId,
randomId = randomId, randomId = randomId,
message = message, message = message,
forward = forward, 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: Long, peerId: Int,
startMessageId: Long? startMessageId: Int?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesMarkAsReadRequest( val requestModel = MessagesMarkAsReadRequest(
peerId = peerId, peerId = peerId,
@@ -229,11 +161,11 @@ class MessagesRepositoryImpl(
} }
override suspend fun getHistoryAttachments( override suspend fun getHistoryAttachments(
peerId: Long, peerId: Int,
count: Int?, count: Int?,
offset: Int?, offset: Int?,
attachmentTypes: List<String>, attachmentTypes: List<String>,
cmId: Long conversationMessageId: Int
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> = ): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryAttachmentsRequest( val requestModel = MessagesGetHistoryAttachmentsRequest(
@@ -243,7 +175,7 @@ class MessagesRepositoryImpl(
offset = offset, offset = offset,
preserveOrder = true, preserveOrder = true,
attachmentTypes = attachmentTypes, attachmentTypes = attachmentTypes,
cmId = cmId, conversationMessageId = conversationMessageId,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
) )
@@ -259,11 +191,6 @@ 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 ->
@@ -273,9 +200,9 @@ class MessagesRepositoryImpl(
} }
override suspend fun createChat( override suspend fun createChat(
userIds: List<Long>?, userIds: List<Int>?,
title: String? title: String?
): ApiResult<Long, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesCreateChatRequest( val requestModel = MessagesCreateChatRequest(
userIds = userIds, userIds = userIds,
title = title title = title
@@ -289,153 +216,81 @@ class MessagesRepositoryImpl(
) )
} }
override suspend fun pin(
peerId: Long,
messageId: Long?,
cmId: Long?
): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesPinMessageRequest(
peerId = peerId,
messageId = messageId,
cmId = 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).mapApiResult(
successMapper = { apiResponse ->
apiResponse.requireResponse().marked.map { it.cmId }
},
errorMapper = { error -> error?.toDomain() }
)
}
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,
cmIds = 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 edit( // override suspend fun markAsImportant(
peerId: Long, // params: MessagesMarkAsImportantRequest
messageId: Long?, // ): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
cmId: Long?, // messagesService.markAsImportant(params.map).mapResult(
message: String?, // successMapper = { response -> response.requireResponse() },
lat: Float?, // errorMapper = { error -> error?.toDomain() }
long: Float?, // )
attachments: List<VkAttachment>?, // }
notParseLinks: Boolean, //
keepSnippets: Boolean, // override suspend fun pin(
keepForwardedMessages: Boolean // params: MessagesPinMessageRequest
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { // ): ApiResult<VkMessageData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesEditRequest( // messagesService.pin(params.map).mapResult(
peerId = peerId, // successMapper = { response -> response.requireResponse() },
messageId = messageId, // errorMapper = { error -> error?.toDomain() }
cmId = cmId, // )
message = message, // }
lat = lat, //
long = long, // override suspend fun unpin(
attachments = attachments, // params: MessagesUnPinMessageRequest
notParseLinks = notParseLinks, // ): ApiResult<Unit, RestApiErrorDomain> = withContext(Dispatchers.IO) {
keepSnippets = keepSnippets, // messagesService.unpin(params.map).mapResult(
keepForwardedMessages = keepForwardedMessages // successMapper = {},
) // errorMapper = { error -> error?.toDomain() }
// )
messagesService.edit(requestModel.map).mapApiDefault() // }
//
// 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() }
// )
// }
} }
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 getConvoMembers(
peerId: Long,
offset: Int?,
count: Int?,
extended: Boolean?,
fields: String?
): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
val requestModel = MessagesGetConvoMembersRequest(
peerId = peerId,
offset = offset,
count = count,
extended = extended,
fields = fields
)
messagesService.getConvoMembers(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()
}
override suspend fun getMessageReadPeers(
peerId: Long,
cmId: Long
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
messagesService.getMessageReadPeers(
mapOf(
"peer_id" to peerId.toString(),
"cmid" to cmId.toString(),
"extended" to "1",
"fields" to VkConstants.USER_FIELDS
)
).mapApiDefault()
}
}
@@ -1,9 +1,6 @@
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 {
@@ -14,15 +11,5 @@ interface OAuthRepository {
validationCode: String?, validationCode: String?,
captchaSid: String?, captchaSid: String?,
captchaKey: String? captchaKey: String?
): ApiResult<AuthDirectResponse, OAuthErrorDomain> ): AuthDirectResponse
suspend fun getSilentToken(
login: String,
password: String,
forceSms: Boolean,
validationCode: String?,
captchaSid: String?,
captchaKey: String?,
successToken: String?
): ApiResult<GetSilentTokenResponse, OAuthErrorDomain>
} }
@@ -1,16 +1,10 @@
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
@@ -24,194 +18,37 @@ class OAuthRepositoryImpl(
forceSms: Boolean, forceSms: Boolean,
validationCode: String?, validationCode: String?,
captchaSid: String?, captchaSid: String?,
captchaKey: String?, captchaKey: String?
): ApiResult<AuthDirectResponse, OAuthErrorDomain> = withContext(Dispatchers.IO) { ): AuthDirectResponse = withContext(Dispatchers.IO) {
val requestModel = AuthDirectRequest( val requestModel = AuthDirectRequest(
grantType = VkConstants.Auth.GrantType.PASSWORD, grantType = VkConstants.Auth.GrantType.PASSWORD,
clientId = VkConstants.MESSENGER_APP_ID.toString(), clientId = VkConstants.VK_APP_ID,
clientSecret = VkConstants.MESSENGER_APP_SECRET, clientSecret = VkConstants.VK_SECRET,
username = login, username = login,
password = password, password = password,
scope = VkConstants.MESSENGER_APP_SCOPE.toString(), scope = VkConstants.Auth.SCOPE,
validationForceSms = forceSms, validationForceSms = forceSms,
validationCode = validationCode, validationCode = validationCode,
captchaSid = captchaSid, captchaSid = captchaSid,
captchaKey = captchaKey, captchaKey = captchaKey,
) )
oAuthService.auth(requestModel.map).mapResult( when (val result = oAuthService.auth(requestModel.map)) {
successMapper = { is ApiResult.Success -> result.value
it
},
errorMapper = { response ->
val error = response?.error?.let(VkOAuthError::parse)
val errorType = response?.errorType?.let(VkOAuthErrorType::parse)
when (error) { is ApiResult.Failure.HttpFailure -> {
null -> OAuthErrorDomain.UnknownError requireNotNull(result.error)
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 -> { is ApiResult.Failure.ApiFailure -> TODO()
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = response.captchaSid.orEmpty(),
captchaImageUrl = response.captchaImage.orEmpty(),
redirectUri = response.redirectUri
)
}
VkOAuthError.INVALID_CLIENT -> { is ApiResult.Failure.NetworkFailure -> {
OAuthErrorDomain.InvalidCredentialsError // TODO: 13/07/2024, Danil Nikolaev: implement showing network error
TODO()
} }
is ApiResult.Failure.UnknownFailure -> TODO()
VkOAuthError.INVALID_REQUEST -> { else -> throw IllegalStateException("Unknown result")
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?,
successToken: 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,
successToken = successToken
)
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(),
redirectUri = response.redirectUri
)
}
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: Long) = suspend fun getMessagesUploadServer(peerId: Int) =
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<Long>?, userIds: List<Int>?,
fields: String?, fields: String?,
nomCase: String? nomCase: String?
): ApiResult<List<VkUser>, RestApiErrorDomain> ): ApiResult<List<VkUser>, RestApiErrorDomain>
suspend fun getLocalUsers(userIds: List<Long>): List<VkUser> suspend fun getLocalUsers(userIds: List<Int>): List<VkUser>
suspend fun storeUsers(users: List<VkUser>) suspend fun storeUsers(users: List<VkUser>)
} }
@@ -1,8 +1,7 @@
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.UserDao import dev.meloda.fast.database.dao.UsersDao
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
@@ -12,17 +11,18 @@ 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: UserDao private val dao: UsersDao
) : UsersRepository { ) : UsersRepository {
override suspend fun get( override suspend fun get(
userIds: List<Long>?, userIds: List<Int>?,
fields: String?, fields: String?,
nomCase: String? nomCase: String?
): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
@@ -38,9 +38,7 @@ class UsersRepositoryImpl(
val users = response.map(VkUserData::mapToDomain) val users = response.map(VkUserData::mapToDomain)
launch(Dispatchers.IO) { launch { storeUsers(users) }
storeUsers(users)
}
VkMemoryCache.appendUsers(users) VkMemoryCache.appendUsers(users)
@@ -53,7 +51,7 @@ class UsersRepositoryImpl(
} }
override suspend fun getLocalUsers( override suspend fun getLocalUsers(
userIds: List<Long> userIds: List<Int>
): 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: Long): AccountEntity? suspend fun getAccountById(userId: Int): 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: Long): AccountEntity? = override suspend fun getAccountById(userId: Int): AccountEntity? =
accountDao.getById(userId) accountDao.getById(userId)
override suspend fun storeAccounts( override suspend fun storeAccounts(
@@ -6,8 +6,8 @@ import dev.meloda.fast.data.api.account.AccountRepositoryImpl
import dev.meloda.fast.data.api.audios.AudiosRepository import dev.meloda.fast.data.api.audios.AudiosRepository
import dev.meloda.fast.data.api.auth.AuthRepository import dev.meloda.fast.data.api.auth.AuthRepository
import dev.meloda.fast.data.api.auth.AuthRepositoryImpl import dev.meloda.fast.data.api.auth.AuthRepositoryImpl
import dev.meloda.fast.data.api.convos.ConvosRepository import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.api.convos.ConvosRepositoryImpl import dev.meloda.fast.data.api.conversations.ConversationsRepositoryImpl
import dev.meloda.fast.data.api.files.FilesRepository import dev.meloda.fast.data.api.files.FilesRepository
import dev.meloda.fast.data.api.friends.FriendsRepository import dev.meloda.fast.data.api.friends.FriendsRepository
import dev.meloda.fast.data.api.friends.FriendsRepositoryImpl import dev.meloda.fast.data.api.friends.FriendsRepositoryImpl
@@ -45,7 +45,7 @@ val dataModule = module {
singleOf(::AuthRepositoryImpl) bind AuthRepository::class singleOf(::AuthRepositoryImpl) bind AuthRepository::class
singleOf(::ConvosRepositoryImpl) bind ConvosRepository::class singleOf(::ConversationsRepositoryImpl) bind ConversationsRepository::class
singleOf(::FilesRepository) singleOf(::FilesRepository)
@@ -65,6 +65,7 @@ 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": "ca007bca2ab4a9b901662792042770ad", "identityHash": "3ebd234270e36902d3d461af38664869",
"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, `exchangeToken` TEXT, PRIMARY KEY(`userId`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, PRIMARY KEY(`userId`))",
"fields": [ "fields": [
{ {
"fieldPath": "userId", "fieldPath": "userId",
@@ -31,12 +31,6 @@
"columnName": "trustedHash", "columnName": "trustedHash",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
},
{
"fieldPath": "exchangeToken",
"columnName": "exchangeToken",
"affinity": "TEXT",
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@@ -52,7 +46,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, 'ca007bca2ab4a9b901662792042770ad')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3ebd234270e36902d3d461af38664869')"
] ]
} }
} }
@@ -1,58 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "ca007bca2ab4a9b901662792042770ad",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, `exchangeToken` TEXT, PRIMARY KEY(`userId`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fastToken",
"columnName": "fastToken",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "trustedHash",
"columnName": "trustedHash",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "exchangeToken",
"columnName": "exchangeToken",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ca007bca2ab4a9b901662792042770ad')"
]
}
}
@@ -1,413 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "6c315b7f800694f635318d86032746ec",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `photo400Orig` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstName",
"columnName": "firstName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastName",
"columnName": "lastName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isOnline",
"columnName": "isOnline",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isOnlineMobile",
"columnName": "isOnlineMobile",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "onlineAppId",
"columnName": "onlineAppId",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT"
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "photo400Orig",
"columnName": "photo400Orig",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "screenName",
"columnName": "screenName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cmId",
"columnName": "cmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT"
},
{
"fieldPath": "isOut",
"columnName": "isOut",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "peerId",
"columnName": "peerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fromId",
"columnName": "fromId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "randomId",
"columnName": "randomId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT"
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT"
},
{
"fieldPath": "actionCmId",
"columnName": "actionCmId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT"
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER"
},
{
"fieldPath": "important",
"columnName": "important",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forwardIds",
"columnName": "forwardIds",
"affinity": "TEXT"
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT"
},
{
"fieldPath": "replyMessageId",
"columnName": "replyMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "geoType",
"columnName": "geoType",
"affinity": "TEXT"
},
{
"fieldPath": "pinnedAt",
"columnName": "pinnedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCmId",
"columnName": "lastCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReadCmId",
"columnName": "inReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outReadCmId",
"columnName": "outReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inRead",
"columnName": "inRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outRead",
"columnName": "outRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
},
{
"fieldPath": "canChangePin",
"columnName": "canChangePin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canChangeInfo",
"columnName": "canChangeInfo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "majorId",
"columnName": "majorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minorId",
"columnName": "minorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinnedMessageId",
"columnName": "pinnedMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "peerType",
"columnName": "peerType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isArchived",
"columnName": "isArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6c315b7f800694f635318d86032746ec')"
]
}
}
@@ -1,413 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "a746865995959331f8a1b512c049dacb",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `photo400Orig` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstName",
"columnName": "firstName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastName",
"columnName": "lastName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isOnline",
"columnName": "isOnline",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isOnlineMobile",
"columnName": "isOnlineMobile",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "onlineAppId",
"columnName": "onlineAppId",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT"
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "photo400Orig",
"columnName": "photo400Orig",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "screenName",
"columnName": "screenName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cmId",
"columnName": "cmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT"
},
{
"fieldPath": "isOut",
"columnName": "isOut",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "peerId",
"columnName": "peerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fromId",
"columnName": "fromId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "randomId",
"columnName": "randomId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT"
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT"
},
{
"fieldPath": "actionCmId",
"columnName": "actionCmId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT"
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER"
},
{
"fieldPath": "important",
"columnName": "important",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forwardIds",
"columnName": "forwardIds",
"affinity": "TEXT"
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT"
},
{
"fieldPath": "replyMessageId",
"columnName": "replyMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "geoType",
"columnName": "geoType",
"affinity": "TEXT"
},
{
"fieldPath": "pinnedAt",
"columnName": "pinnedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "convos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCmId",
"columnName": "lastCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReadCmId",
"columnName": "inReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outReadCmId",
"columnName": "outReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inRead",
"columnName": "inRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outRead",
"columnName": "outRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
},
{
"fieldPath": "canChangePin",
"columnName": "canChangePin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canChangeInfo",
"columnName": "canChangeInfo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "majorId",
"columnName": "majorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minorId",
"columnName": "minorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinnedMessageId",
"columnName": "pinnedMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "peerType",
"columnName": "peerType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isArchived",
"columnName": "isArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a746865995959331f8a1b512c049dacb')"
]
}
}
@@ -1,425 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "5eca3b3da167aaf7e772977a1f4e56e2",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `photo400Orig` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstName",
"columnName": "firstName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastName",
"columnName": "lastName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isOnline",
"columnName": "isOnline",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isOnlineMobile",
"columnName": "isOnlineMobile",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "onlineAppId",
"columnName": "onlineAppId",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT"
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "photo400Orig",
"columnName": "photo400Orig",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "screenName",
"columnName": "screenName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `isImportant` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `isSpam` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cmId",
"columnName": "cmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT"
},
{
"fieldPath": "isOut",
"columnName": "isOut",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "peerId",
"columnName": "peerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fromId",
"columnName": "fromId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "randomId",
"columnName": "randomId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT"
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT"
},
{
"fieldPath": "actionCmId",
"columnName": "actionCmId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT"
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER"
},
{
"fieldPath": "isImportant",
"columnName": "isImportant",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forwardIds",
"columnName": "forwardIds",
"affinity": "TEXT"
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT"
},
{
"fieldPath": "replyMessageId",
"columnName": "replyMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "geoType",
"columnName": "geoType",
"affinity": "TEXT"
},
{
"fieldPath": "pinnedAt",
"columnName": "pinnedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDeleted",
"columnName": "isDeleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSpam",
"columnName": "isSpam",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "convos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCmId",
"columnName": "lastCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReadCmId",
"columnName": "inReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outReadCmId",
"columnName": "outReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inRead",
"columnName": "inRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outRead",
"columnName": "outRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
},
{
"fieldPath": "canChangePin",
"columnName": "canChangePin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canChangeInfo",
"columnName": "canChangeInfo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "majorId",
"columnName": "majorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minorId",
"columnName": "minorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinnedMessageId",
"columnName": "pinnedMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "peerType",
"columnName": "peerType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isArchived",
"columnName": "isArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5eca3b3da167aaf7e772977a1f4e56e2')"
]
}
}
@@ -7,7 +7,7 @@ import dev.meloda.fast.model.database.AccountEntity
@Database( @Database(
entities = [AccountEntity::class], entities = [AccountEntity::class],
version = 3 version = 2
) )
abstract class AccountsDatabase : RoomDatabase() { abstract class AccountsDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao abstract fun accountDao(): AccountDao
@@ -3,12 +3,12 @@ package dev.meloda.fast.database
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import dev.meloda.fast.database.dao.ConvoDao import dev.meloda.fast.database.dao.ConversationDao
import dev.meloda.fast.database.dao.GroupDao import dev.meloda.fast.database.dao.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.database.dao.UsersDao
import dev.meloda.fast.database.typeconverters.Converters import dev.meloda.fast.database.typeconverters.Converters
import dev.meloda.fast.model.database.VkConvoEntity import dev.meloda.fast.model.database.VkConversationEntity
import dev.meloda.fast.model.database.VkGroupEntity import dev.meloda.fast.model.database.VkGroupEntity
import dev.meloda.fast.model.database.VkMessageEntity import dev.meloda.fast.model.database.VkMessageEntity
import dev.meloda.fast.model.database.VkUserEntity import dev.meloda.fast.model.database.VkUserEntity
@@ -18,15 +18,15 @@ import dev.meloda.fast.model.database.VkUserEntity
VkUserEntity::class, VkUserEntity::class,
VkGroupEntity::class, VkGroupEntity::class,
VkMessageEntity::class, VkMessageEntity::class,
VkConvoEntity::class VkConversationEntity::class
], ],
version = 12 version = 7
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class CacheDatabase : RoomDatabase() { abstract class CacheDatabase : RoomDatabase() {
abstract fun userDao(): UserDao abstract fun userDao(): UsersDao
abstract fun groupDao(): GroupDao abstract fun groupDao(): GroupDao
abstract fun messageDao(): MessageDao abstract fun messageDao(): MessageDao
abstract fun convoDao(): ConvoDao 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: Long): AccountEntity? abstract suspend fun getById(userId: Int): AccountEntity?
@Query("DELETE FROM accounts WHERE userId = :userId") @Query("DELETE FROM accounts WHERE userId = :userId")
abstract suspend fun deleteById(userId: Long) abstract suspend fun deleteById(userId: Int)
} }
@@ -0,0 +1,30 @@
package dev.meloda.fast.database.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import dev.meloda.fast.model.database.ConversationWithMessage
import dev.meloda.fast.model.database.VkConversationEntity
@Dao
abstract class ConversationDao : EntityDao<VkConversationEntity> {
@Query("SELECT * FROM conversations")
abstract suspend fun getAll(): List<VkConversationEntity>
@Query("SELECT * FROM conversations WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConversationEntity>
@Query("SELECT * FROM conversations WHERE id IS (:id)")
abstract suspend fun getById(id: Int): VkConversationEntity?
@Transaction
@Query("SELECT * FROM conversations WHERE id IS (:id)")
abstract suspend fun getByIdWithMessage(id: Int): ConversationWithMessage?
@Query("DELETE FROM conversations WHERE rowid IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int
}
@@ -1,45 +0,0 @@
package dev.meloda.fast.database.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import dev.meloda.fast.model.database.ConvoWithMessage
import dev.meloda.fast.model.database.VkConvoEntity
@Dao
abstract class ConvoDao : EntityDao<VkConvoEntity> {
@Query("SELECT * FROM convos")
abstract suspend fun getAll(): List<VkConvoEntity>
@Query("SELECT * FROM convos WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Long>): List<VkConvoEntity>
@Query("SELECT * FROM convos WHERE id IS (:id)")
abstract suspend fun getById(id: Long): VkConvoEntity?
@Transaction
@Query("SELECT * FROM convos WHERE id IS (:id)")
abstract suspend fun getByIdWithMessage(id: Long): ConvoWithMessage?
@Query("DELETE FROM convos WHERE rowid IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Long>): Int
@Query("UPDATE convos SET inReadCmId = :cmId, unreadCount = :unreadCount WHERE id = :convoId")
abstract suspend fun updateReadIncoming(convoId: Long, cmId: Long, unreadCount: Int): Int
@Query("UPDATE convos SET outReadCmId = :cmId, unreadCount = :unreadCount WHERE id = :convoId")
abstract suspend fun updateReadOutgoing(convoId: Long, cmId: Long, unreadCount: Int): Int
@Query("UPDATE convos SET isArchived = :isArchived WHERE id = :convoId")
abstract suspend fun updateIsArchived(convoId: Long, isArchived: Boolean): Int
@Query("UPDATE convos SET majorId = :majorId WHERE id = :convoId")
abstract suspend fun updateMajorId(convoId: Long, majorId: Int): Int
@Query("UPDATE convos SET minorId = :minorId WHERE id = :convoId")
abstract suspend fun updateMinorId(convoId: Long, minorId: Int): Int
@Query("UPDATE convos SET lastCmId = :cmId WHERE id = :convoId")
abstract suspend fun updateLastCmId(convoId: Long, cmId: Long): Int
}
@@ -7,27 +7,18 @@ import dev.meloda.fast.model.database.VkMessageEntity
@Dao @Dao
abstract class MessageDao : EntityDao<VkMessageEntity> { abstract class MessageDao : EntityDao<VkMessageEntity> {
@Query("SELECT * FROM messages WHERE isDeleted = 0 AND isSpam = 0") @Query("SELECT * FROM messages")
abstract suspend fun getAll(): List<VkMessageEntity> abstract suspend fun getAll(): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE peerId IS (:convoId)") @Query("SELECT * FROM messages WHERE peerId IS (:conversationId)")
abstract suspend fun getAll(convoId: Long): List<VkMessageEntity> abstract suspend fun getAll(conversationId: Int): 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: Long): VkMessageEntity? abstract suspend fun getById(messageId: Int): 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
@Query("UPDATE messages SET isDeleted = :isDeleted WHERE peerId = :convoId AND cmId = :cmId")
abstract suspend fun markAsDeleted(convoId: Long, cmId: Long, isDeleted: Boolean): Int
@Query("UPDATE messages SET isImportant = :isImportant WHERE peerId = :convoId AND cmId = :cmId")
abstract suspend fun markAsImportant(convoId: Long, cmId: Long, isImportant: Boolean): Int
@Query("UPDATE messages SET isSpam = :isSpam WHERE peerId = :convoId AND cmId = :cmId")
abstract suspend fun markAsSpam(convoId: Long, cmId: Long, isSpam: Boolean): 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 UserDao : EntityDao<VkUserEntity> { abstract class UsersDao : 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<Long>): List<VkUserEntity> abstract suspend fun getAllByIds(ids: List<Int>): List<VkUserEntity>
@Query("DELETE FROM users WHERE id IN (:ids)") @Query("DELETE FROM users WHERE id IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Long>): Int abstract suspend fun deleteByIds(ids: List<Int>): Int
} }
@@ -2,28 +2,24 @@ 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") Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts").build()
.addMigrations(migrationFrom2To3)
.build()
} }
single { get<AccountsDatabase>().accountDao() } single { get<AccountsDatabase>().accountDao() }
single { single {
Room.databaseBuilder(get(), CacheDatabase::class.java, "cache") Room.databaseBuilder(get(), dev.meloda.fast.database.CacheDatabase::class.java, "cache")
.fallbackToDestructiveMigration(true) .fallbackToDestructiveMigration()
.build() .build()
} }
single { cacheDB().userDao() } single { cacheDB().userDao() }
single { cacheDB().groupDao() } single { cacheDB().groupDao() }
single { cacheDB().messageDao() } single { cacheDB().messageDao() }
single { cacheDB().convoDao() } single { cacheDB().conversationDao() }
} }
private fun Scope.cacheDB(): CacheDatabase = get() private fun Scope.cacheDB(): dev.meloda.fast.database.CacheDatabase = get()
@@ -1,14 +0,0 @@
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,15 +13,6 @@ 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()
@@ -3,33 +3,14 @@ package dev.meloda.fast.datastore
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import dev.meloda.fast.common.model.DarkMode import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.NetworkLogLevel import dev.meloda.fast.common.model.LogLevel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.reflect.KClass import kotlin.reflect.KClass
sealed class CaptchaTokenResult {
data object Initial : CaptchaTokenResult()
data object Null : CaptchaTokenResult()
data object Cancelled : CaptchaTokenResult()
data class Success(val token: String) : CaptchaTokenResult()
}
object AppSettings { object AppSettings {
private var preferences: SharedPreferences by Delegates.notNull() private var preferences: SharedPreferences by Delegates.notNull()
private val captchaResult = MutableStateFlow<CaptchaTokenResult>(CaptchaTokenResult.Initial)
fun getCaptchaResultFlow(): StateFlow<CaptchaTokenResult> = captchaResult.asStateFlow()
fun setCaptchaResult(result: CaptchaTokenResult) = captchaResult.update { result }
private val captchaRedirectUri = MutableStateFlow<String?>(null)
fun getCaptchaRedirectUriFlow() = captchaRedirectUri.asStateFlow()
fun setCaptchaRedirectUri(redirectUri: String?) = captchaRedirectUri.update { redirectUri }
fun init(preferences: SharedPreferences) { fun init(preferences: SharedPreferences) {
this.preferences = preferences this.preferences = preferences
} }
@@ -115,20 +96,6 @@ object AppSettings {
) )
set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value) set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
var showAttachmentButton: Boolean
get() = get(
SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON,
SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON
)
set(value) = put(SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, value)
var showManualRefreshOptions: Boolean
get() = get(
SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS,
SettingsKeys.DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS
)
set(value) = put(SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS, value)
var enableHaptic: Boolean var enableHaptic: Boolean
get() = get( get() = get(
SettingsKeys.KEY_ENABLE_HAPTIC, SettingsKeys.KEY_ENABLE_HAPTIC,
@@ -238,11 +205,11 @@ object AppSettings {
) )
set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value) set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value)
var networkLogLevel: NetworkLogLevel var networkLogLevel: LogLevel
get() = get( get() = get(
SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL,
SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL
).let(NetworkLogLevel::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 showDebugCategory: Boolean var showDebugCategory: Boolean
@@ -11,10 +11,6 @@ object SettingsKeys {
const val DEFAULT_VALUE_USE_CONTACT_NAMES = false const val DEFAULT_VALUE_USE_CONTACT_NAMES = false
const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button" const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button"
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
const val KEY_SHOW_ATTACHMENT_BUTTON = "show_attachment_button"
const val DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON = false
const val KEY_SHOW_MANUAL_REFRESH_OPTIONS = "show_manual_refresh_options"
const val DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS = false
const val KEY_APPEARANCE = "appearance" const val KEY_APPEARANCE = "appearance"
const val KEY_APPEARANCE_MULTILINE = "appearance_multiline" const val KEY_APPEARANCE_MULTILINE = "appearance_multiline"
@@ -49,10 +45,7 @@ object SettingsKeys {
const val KEY_ENABLE_HAPTIC = "enable_haptic" const val KEY_ENABLE_HAPTIC = "enable_haptic"
const val DEFAULT_ENABLE_HAPTIC = true const val DEFAULT_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 = 3 const val DEFAULT_NETWORK_LOG_LEVEL = 0
const val KEY_DEBUG_IMPORT_AUTH_DATA = "debug_import_auth_data"
const val KEY_DEBUG_EXPORT_AUTH_DATA = "debug_export_auth_data"
const val KEY_USE_SYSTEM_FONT = "use_system_font" const val KEY_USE_SYSTEM_FONT = "use_system_font"
const val DEFAULT_USE_SYSTEM_FONT = false const val DEFAULT_USE_SYSTEM_FONT = false
const val KEY_MORE_ANIMATIONS = "more_animations" const val KEY_MORE_ANIMATIONS = "more_animations"
@@ -60,5 +53,5 @@ object SettingsKeys {
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
} }
@@ -14,12 +14,16 @@ interface UserSettings {
val enableDynamicColors: StateFlow<Boolean> val enableDynamicColors: StateFlow<Boolean>
val appLanguage: StateFlow<String> val appLanguage: StateFlow<String>
val fastText: StateFlow<String>
val sendOnlineStatus: StateFlow<Boolean> val sendOnlineStatus: StateFlow<Boolean>
val showAlertAfterCrash: StateFlow<Boolean>
val longPollInBackground: StateFlow<Boolean> val longPollInBackground: StateFlow<Boolean>
val useBlur: StateFlow<Boolean> val useBlur: StateFlow<Boolean>
val showEmojiButton: 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)
@@ -30,10 +34,15 @@ interface UserSettings {
fun onEnableDynamicColorsChanged(enable: Boolean) fun onEnableDynamicColorsChanged(enable: Boolean)
fun onAppLanguageChanged(language: String) fun onAppLanguageChanged(language: String)
fun onFastTextChanged(text: String)
fun onSendOnlineStatusChanged(send: Boolean) fun onSendOnlineStatusChanged(send: Boolean)
fun onShowAlertAfterCrashChanged(show: Boolean)
fun onLongPollInBackgroundChanged(inBackground: Boolean) fun onLongPollInBackgroundChanged(inBackground: Boolean)
fun onUseBlurChanged(use: Boolean) fun onUseBlurChanged(use: Boolean)
fun onShowEmojiButtonChanged(show: Boolean)
fun onShowTimeInActionMessagesChanged(show: Boolean)
fun onUseSystemFontChanged(use: Boolean) fun onUseSystemFontChanged(use: Boolean)
fun onShowDebugCategoryChanged(show: Boolean) fun onShowDebugCategoryChanged(show: Boolean)
} }
@@ -48,13 +57,17 @@ class UserSettingsImpl : UserSettings {
override val enableDynamicColors = MutableStateFlow(AppSettings.Appearance.enableDynamicColors) override val enableDynamicColors = MutableStateFlow(AppSettings.Appearance.enableDynamicColors)
override val appLanguage = MutableStateFlow(AppSettings.Appearance.appLanguage) override val appLanguage = MutableStateFlow(AppSettings.Appearance.appLanguage)
override val fastText = MutableStateFlow(AppSettings.Features.fastText)
override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus) override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus)
override val longPollInBackground = override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash)
MutableStateFlow(AppSettings.Experimental.longPollInBackground) override val longPollInBackground = MutableStateFlow(AppSettings.Experimental.longPollInBackground)
override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur) override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur)
override val showEmojiButton = MutableStateFlow(AppSettings.General.showEmojiButton)
override val showTimeInActionMessages =
MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages)
override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont) override val 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) {
@@ -81,10 +94,18 @@ class UserSettingsImpl : UserSettings {
appLanguage.value = language appLanguage.value = language
} }
override fun onFastTextChanged(text: String) {
fastText.value = text
}
override fun onSendOnlineStatusChanged(send: Boolean) { override fun onSendOnlineStatusChanged(send: Boolean) {
sendOnlineStatus.value = send sendOnlineStatus.value = send
} }
override fun onShowAlertAfterCrashChanged(show: Boolean) {
showAlertAfterCrash.value = show
}
override fun onLongPollInBackgroundChanged(inBackground: Boolean) { override fun onLongPollInBackgroundChanged(inBackground: Boolean) {
longPollInBackground.value = inBackground longPollInBackground.value = inBackground
} }
@@ -93,6 +114,14 @@ class UserSettingsImpl : UserSettings {
useBlur.value = use useBlur.value = use
} }
override fun onShowEmojiButtonChanged(show: Boolean) {
showEmojiButton.value = show
}
override fun onShowTimeInActionMessagesChanged(show: Boolean) {
showTimeInActionMessages.value = show
}
override fun onUseSystemFontChanged(use: Boolean) { override fun onUseSystemFontChanged(use: Boolean) {
useSystemFont.value = use useSystemFont.value = use
} }
+1 -6
View File
@@ -8,16 +8,11 @@ android {
} }
dependencies { dependencies {
api(projects.core.common)
api(projects.core.data) api(projects.core.data)
api(projects.core.model) api(projects.core.model)
// TODO: 11/08/2024, Danil Nikolaev: remove?
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.koin.core) implementation(libs.koin.core)
implementation(libs.eithernet) implementation(libs.eithernet)
implementation(libs.bundles.nanokt)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
} }
@@ -1,32 +1,12 @@
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 : BaseUseCase { interface AuthUseCase {
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,44 +3,16 @@ 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 logout(): Flow<State<Int>> = flowNewState { repository.logout().mapToState() } override fun validatePhone(validationSid: String): Flow<State<ValidatePhoneResponse>> = flow {
emit(State.Loading)
override fun validatePhone(validationSid: String): Flow<State<ValidatePhoneResponse>> = val newState = repository.validatePhone(validationSid).mapToState()
flowNewState { repository.validatePhone(validationSid = validationSid).mapToState() } emit(newState)
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()
} }
} }
@@ -1,16 +0,0 @@
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) }
}
@@ -0,0 +1,19 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.flow.Flow
interface ConversationsUseCase {
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>)
}
@@ -0,0 +1,118 @@
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.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
class ConversationsUseCaseImpl(
private val repository: ConversationsRepository,
) : 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(
conversations: List<VkConversation>
) = withContext(Dispatchers.IO) {
repository.storeConversations(conversations)
}
override fun delete(peerId: Int): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.delete(peerId = peerId).mapToState()
emit(newState)
}
override fun changePinState(peerId: Int, pin: Boolean): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = if (pin) {
repository.pin(peerId)
} else {
repository.unpin(peerId)
}.mapToState()
emit(newState)
}
}
@@ -1,29 +0,0 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
import kotlinx.coroutines.flow.Flow
interface ConvoUseCase : BaseUseCase {
suspend fun storeConvos(convos: List<VkConvo>)
fun getConvos(
count: Int? = null,
offset: Int? = null,
filter: ConvosFilter
): Flow<State<List<VkConvo>>>
fun getById(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): Flow<State<List<VkConvo>>>
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>>
}
@@ -1,71 +0,0 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.convos.ConvosRepository
import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class ConvoUseCaseImpl(
private val repository: ConvosRepository,
) : ConvoUseCase {
override suspend fun storeConvos(
convos: List<VkConvo>
) = withContext(Dispatchers.IO) {
repository.storeConvos(convos)
}
override fun getConvos(
count: Int?,
offset: Int?,
filter: ConvosFilter
): Flow<State<List<VkConvo>>> = flowNewState {
repository.getConvos(
count = count,
offset = offset,
filter = filter
).mapToState()
}
override fun getById(
peerIds: List<Long>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkConvo>>> = flowNewState {
repository.getConvosById(
peerIds = peerIds,
extended = extended,
fields = fields
).mapToState()
}
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)
} else {
repository.unpin(peerId)
}.mapToState()
}
override fun changeArchivedState(
peerId: Long,
archive: Boolean
): Flow<State<Int>> = flowNewState {
if (archive) {
repository.archive(peerId)
} else {
repository.unarchive(peerId)
}.mapToState()
}
}

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