Upstream changes (#23)
@@ -0,0 +1,48 @@
|
|||||||
|
name: Android CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ "dev" ]
|
||||||
|
push:
|
||||||
|
branches: [ "dev" ]
|
||||||
|
|
||||||
|
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:
|
||||||
|
build_apk_aab:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Build dev artifacts
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Build and sign debug APKs
|
||||||
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
- name: Build and sign release APKs
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
|
- name: Upload dev-debug APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-dev-debug.apk
|
||||||
|
path: app/build/outputs/apk/amethyst/debug/app-amethyst-debug.apk
|
||||||
|
|
||||||
|
- name: Upload dev-release APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-dev-release.apk
|
||||||
|
path: app/build/outputs/apk/amethyst/release/app-amethyst-release.apk
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Android CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}
|
||||||
|
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_apk_aab:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Build full artifacts
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Build and sign debug APKs
|
||||||
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
- name: Build and sign release APKs
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
|
- name: Upload full-debug APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-full-debug.apk
|
||||||
|
path: app/build/outputs/apk/full/debug/app-full-debug.apk
|
||||||
|
|
||||||
|
- name: Upload full-release APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-full-release.apk
|
||||||
|
path: app/build/outputs/apk/full/release/app-full-release.apk
|
||||||
@@ -8,9 +8,10 @@
|
|||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
build/
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
.idea
|
.idea
|
||||||
|
/.kotlin
|
||||||
|
|||||||
@@ -1,3 +1,82 @@
|
|||||||
|
### Module Graph
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{
|
||||||
|
init: {
|
||||||
|
'theme': 'base',
|
||||||
|
'themeVariables': {"primaryTextColor":"#fff","primaryColor":"#5a4f7c","primaryBorderColor":"#5a4f7c","lineColor":"#f5a623","tertiaryColor":"#40375c","fontSize":"12px"}
|
||||||
|
}
|
||||||
|
}%%
|
||||||
|
|
||||||
|
graph LR
|
||||||
|
subgraph :core
|
||||||
|
:core:database["database"]
|
||||||
|
:core:model["model"]
|
||||||
|
:core:data["data"]
|
||||||
|
:core:ui["ui"]
|
||||||
|
:core:common["common"]
|
||||||
|
:core:designsystem["designsystem"]
|
||||||
|
:core:datastore["datastore"]
|
||||||
|
:core:network["network"]
|
||||||
|
end
|
||||||
|
subgraph :feature
|
||||||
|
:feature:chatmaterials["chatmaterials"]
|
||||||
|
:feature:messageshistory["messageshistory"]
|
||||||
|
:feature:settings["settings"]
|
||||||
|
:feature:languagepicker["languagepicker"]
|
||||||
|
:feature:userbanned["userbanned"]
|
||||||
|
:feature:auth["auth"]
|
||||||
|
:feature:conversations["conversations"]
|
||||||
|
:feature:photoviewer["photoviewer"]
|
||||||
|
end
|
||||||
|
:core:database --> :core:model
|
||||||
|
:feature:chatmaterials --> :core:data
|
||||||
|
:feature:chatmaterials --> :core:model
|
||||||
|
:feature:chatmaterials --> :core:ui
|
||||||
|
:feature:messageshistory --> :core:data
|
||||||
|
:feature:messageshistory --> :core:model
|
||||||
|
:feature:messageshistory --> :core:ui
|
||||||
|
:feature:settings --> :core:data
|
||||||
|
:feature:settings --> :core:model
|
||||||
|
:feature:settings --> :core:ui
|
||||||
|
:feature:languagepicker --> :core:data
|
||||||
|
:feature:languagepicker --> :core:model
|
||||||
|
:feature:languagepicker --> :core:ui
|
||||||
|
:feature:userbanned --> :core:data
|
||||||
|
:feature:userbanned --> :core:model
|
||||||
|
:feature:userbanned --> :core:ui
|
||||||
|
:app --> :feature:auth
|
||||||
|
:app --> :feature:chatmaterials
|
||||||
|
:app --> :feature:conversations
|
||||||
|
:app --> :feature:languagepicker
|
||||||
|
:app --> :feature:messageshistory
|
||||||
|
:app --> :feature:photoviewer
|
||||||
|
:app --> :feature:settings
|
||||||
|
:app --> :feature:userbanned
|
||||||
|
:app --> :core:common
|
||||||
|
:app --> :core:ui
|
||||||
|
:app --> :core:designsystem
|
||||||
|
:app --> :core:data
|
||||||
|
:app --> :core:model
|
||||||
|
:app --> :core:datastore
|
||||||
|
:core:data --> :core:common
|
||||||
|
:core:data --> :core:model
|
||||||
|
:core:data --> :core:network
|
||||||
|
:core:data --> :core:database
|
||||||
|
:core:network --> :core:common
|
||||||
|
:core:network --> :core:model
|
||||||
|
:feature:auth --> :core:data
|
||||||
|
:feature:auth --> :core:ui
|
||||||
|
:core:ui --> :core:designsystem
|
||||||
|
:core:ui --> :core:model
|
||||||
|
:feature:photoviewer --> :core:data
|
||||||
|
:feature:photoviewer --> :core:model
|
||||||
|
:feature:photoviewer --> :core:ui
|
||||||
|
:feature:conversations --> :core:data
|
||||||
|
:feature:conversations --> :core:model
|
||||||
|
:feature:conversations --> :core:ui
|
||||||
|
:core:datastore --> :core:common
|
||||||
|
```
|
||||||
# fast-messenger
|
# fast-messenger
|
||||||
|
|
||||||
Unofficial messenger for russian social network VKontakte
|
Unofficial messenger for russian social network VKontakte
|
||||||
@@ -1 +1,3 @@
|
|||||||
/build
|
/build
|
||||||
|
/keystore/keystore.properties
|
||||||
|
/full
|
||||||
|
|||||||
@@ -1,226 +1,184 @@
|
|||||||
@file:Suppress("UnstableApiUsage")
|
import java.util.Properties
|
||||||
|
|
||||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
|
||||||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
|
||||||
|
|
||||||
val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage", "\"\"")
|
|
||||||
val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint", "\"\"")
|
|
||||||
|
|
||||||
val msAppCenterToken: String =
|
|
||||||
gradleLocalProperties(rootDir).getProperty("msAppCenterAppToken", "\"\"")
|
|
||||||
val otaSecretCode: String = gradleLocalProperties(rootDir).getProperty("otaSecretCode", "\"\"")
|
|
||||||
|
|
||||||
val majorVersion = 1
|
|
||||||
val minorVersion = 6
|
|
||||||
val patchVersion = 4
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
alias(libs.plugins.com.android.application)
|
||||||
id("kotlin-android")
|
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||||
id("kotlin-kapt")
|
alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize)
|
||||||
id("kotlin-parcelize")
|
alias(libs.plugins.com.google.devtools.ksp)
|
||||||
id("org.jetbrains.kotlin.android")
|
alias(libs.plugins.kotlin.compose.compiler)
|
||||||
id("com.google.devtools.ksp")
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.meloda.fast"
|
namespace = "com.meloda.app.fast"
|
||||||
|
compileSdk = Configs.compileSdk
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
applicationVariants.all {
|
|
||||||
outputs.all {
|
|
||||||
(this as BaseVariantOutputImpl).outputFileName =
|
|
||||||
"${name}-${versionName}-${versionCode}.apk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.meloda.fast"
|
applicationId = "com.meloda.app.fast"
|
||||||
minSdk = 24
|
minSdk = Configs.minSdk
|
||||||
targetSdk = 34
|
targetSdk = Configs.targetSdk
|
||||||
versionCode = 1
|
versionCode = Configs.appCode
|
||||||
versionName = "alpha"
|
versionName = Configs.appName
|
||||||
|
|
||||||
javaCompileOptions {
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
annotationProcessorOptions {
|
|
||||||
// arguments += mapOf("room.schemaLocation" to "$projectDir/schemas")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 06/05/2024, Danil Nikolaev: придумать, как совместить с github actions
|
||||||
|
// applicationVariants.all {
|
||||||
|
// val variant = this
|
||||||
|
// variant.outputs
|
||||||
|
// .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||||
|
// .forEach { output ->
|
||||||
|
// if (variant.buildType.name == "release") {
|
||||||
|
// val outputFileName = "fastvk-v${variant.versionName}-${variant.flavorName}.apk"
|
||||||
|
// output.outputFileName = outputFileName
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
val keystorePropertiesFile = file("keystore/keystore.properties")
|
||||||
|
|
||||||
|
storeFile = file("keystore/keystore.jks")
|
||||||
|
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystorePropertiesFile.inputStream().let(keystoreProperties::load)
|
||||||
|
storePassword = keystoreProperties.getProperty("storePassword")
|
||||||
|
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||||
|
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||||
|
} else {
|
||||||
|
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||||
|
keyAlias = System.getenv("RELEASE_SIGN_KEY_ALIAS")
|
||||||
|
keyPassword = System.getenv("RELEASE_SIGN_KEY_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create("debugSigning") {
|
||||||
|
initWith(getByName("release"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("debug") {
|
named("debug") {
|
||||||
buildConfigField("String", "sdkPackage", sdkPackage)
|
signingConfig = signingConfigs.getByName("debugSigning")
|
||||||
buildConfigField("String", "sdkFingerprint", sdkFingerprint)
|
applicationIdSuffix = ".debug"
|
||||||
|
|
||||||
buildConfigField("String", "msAppCenterAppToken", msAppCenterToken)
|
|
||||||
|
|
||||||
buildConfigField("String", "otaSecretCode", otaSecretCode)
|
|
||||||
|
|
||||||
versionNameSuffix = "_${getVersionName()}"
|
|
||||||
}
|
}
|
||||||
getByName("release") {
|
named("release") {
|
||||||
isMinifyEnabled = false
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
|
||||||
buildConfigField("String", "sdkPackage", sdkPackage)
|
isMinifyEnabled = true
|
||||||
buildConfigField("String", "sdkFingerprint", sdkFingerprint)
|
isShrinkResources = true
|
||||||
|
|
||||||
buildConfigField("String", "msAppCenterAppToken", msAppCenterToken)
|
|
||||||
|
|
||||||
buildConfigField("String", "otaSecretCode", otaSecretCode)
|
|
||||||
|
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 15/05/2024, Danil Nikolaev: add to other modules with build convention
|
||||||
|
register("staging") {
|
||||||
|
initWith(getByName("release"))
|
||||||
|
applicationIdSuffix = ".staging"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val flavorDimension = "version"
|
val flavorDimension = "variant"
|
||||||
|
|
||||||
flavorDimensions += flavorDimension
|
flavorDimensions += flavorDimension
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("dev") {
|
register("amethyst") {
|
||||||
resourceConfigurations += listOf("en", "xxhdpi")
|
|
||||||
|
|
||||||
dimension = flavorDimension
|
|
||||||
applicationIdSuffix = ".dev"
|
|
||||||
versionNameSuffix = "-dev"
|
|
||||||
}
|
|
||||||
create("full") {
|
|
||||||
dimension = flavorDimension
|
dimension = flavorDimension
|
||||||
|
isDefault = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = Configs.java
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = Configs.java
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
jvmTarget = Configs.java.toString()
|
||||||
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
|
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
|
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.4.5"
|
|
||||||
useLiveLiterals = true
|
useLiveLiterals = true
|
||||||
}
|
}
|
||||||
packagingOptions {
|
|
||||||
jniLibs {
|
|
||||||
useLegacyPackaging = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kapt {
|
// packaging {
|
||||||
correctErrorTypes = true
|
// resources {
|
||||||
|
// excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion"
|
|
||||||
|
|
||||||
val currentTime get() = (System.currentTimeMillis() / 1000).toInt()
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(projects.feature.auth)
|
||||||
|
implementation(projects.feature.chatmaterials)
|
||||||
|
implementation(projects.feature.conversations)
|
||||||
|
implementation(projects.feature.languagepicker)
|
||||||
|
implementation(projects.feature.messageshistory)
|
||||||
|
implementation(projects.feature.photoviewer)
|
||||||
|
implementation(projects.feature.settings)
|
||||||
|
implementation(projects.feature.friends)
|
||||||
|
implementation(projects.feature.profile)
|
||||||
|
|
||||||
|
implementation(projects.core.common)
|
||||||
|
implementation(projects.core.ui)
|
||||||
|
implementation(projects.core.designsystem)
|
||||||
|
implementation(projects.core.data)
|
||||||
|
implementation(projects.core.model)
|
||||||
|
implementation(projects.core.datastore)
|
||||||
|
|
||||||
// DI zone
|
// Tests zone
|
||||||
implementation("io.insert-koin:koin-android:3.4.0")
|
testImplementation(libs.junit)
|
||||||
// end of DI zone
|
// end of Tests zone
|
||||||
|
|
||||||
implementation("com.github.skydoves:cloudy:0.1.2")
|
// Compose-Bom zone
|
||||||
|
implementation(platform(libs.compose.bom))
|
||||||
|
implementation(libs.bundles.compose)
|
||||||
|
// end of Compose-Bom zone
|
||||||
|
|
||||||
implementation("io.coil-kt:coil-compose:2.3.0")
|
implementation(libs.accompanist.permissions)
|
||||||
implementation("io.coil-kt:coil:2.3.0")
|
|
||||||
|
|
||||||
implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2")
|
// Coil for Compose
|
||||||
implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2")
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.21")
|
androidTestImplementation(libs.compose.ui.test.junit4)
|
||||||
|
debugImplementation(libs.compose.ui.test.manifest)
|
||||||
|
debugImplementation(libs.compose.ui.tooling)
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.10.1")
|
implementation(libs.koin.android)
|
||||||
|
implementation(libs.koin.androidx.compose)
|
||||||
|
implementation(libs.koin.androidx.compose.navigation)
|
||||||
|
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
implementation(libs.coil)
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
|
||||||
|
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation(libs.core.ktx)
|
||||||
|
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation(libs.lifecycle.viewmodel.ktx)
|
||||||
|
implementation(libs.lifecycle.runtime.ktx)
|
||||||
|
|
||||||
implementation("androidx.activity:activity-ktx:1.7.2")
|
implementation(libs.preference.ktx)
|
||||||
|
implementation(libs.material)
|
||||||
|
|
||||||
implementation("androidx.fragment:fragment-ktx:1.6.1")
|
implementation(libs.haze)
|
||||||
|
implementation(libs.haze.materials)
|
||||||
|
|
||||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation(libs.nanokt)
|
||||||
|
implementation(libs.nanokt.android)
|
||||||
|
implementation(libs.nanokt.jvm)
|
||||||
|
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
implementation(libs.kotlin.serialization)
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
|
||||||
|
|
||||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0")
|
|
||||||
|
|
||||||
implementation("androidx.room:room-ktx:2.5.2")
|
|
||||||
implementation("androidx.room:room-runtime:2.5.2")
|
|
||||||
ksp("androidx.room:room-compiler:2.5.2")
|
|
||||||
|
|
||||||
implementation("com.github.terrakok:cicerone:7.1")
|
|
||||||
|
|
||||||
implementation("com.github.massoudss:waveformSeekBar:5.0.0")
|
|
||||||
|
|
||||||
implementation("com.github.bumptech.glide:glide:4.15.1")
|
|
||||||
ksp("com.github.bumptech.glide:compiler:4.15.1")
|
|
||||||
|
|
||||||
implementation("com.github.fondesa:kpermissions:3.4.0")
|
|
||||||
implementation("com.github.fondesa:kpermissions-coroutines:3.4.0")
|
|
||||||
|
|
||||||
implementation("com.microsoft.appcenter:appcenter-analytics:5.0.1")
|
|
||||||
implementation("com.microsoft.appcenter:appcenter-crashes:5.0.1")
|
|
||||||
|
|
||||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
|
||||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
|
||||||
|
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11")
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.7.1")
|
|
||||||
|
|
||||||
implementation("com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.9")
|
|
||||||
|
|
||||||
implementation("com.google.code.gson:gson:2.10.1")
|
|
||||||
|
|
||||||
implementation("com.google.guava:guava:31.1-jre")
|
|
||||||
|
|
||||||
implementation("com.google.android.material:material:1.9.0")
|
|
||||||
|
|
||||||
implementation("com.github.chuckerteam.chucker:library:3.5.2")
|
|
||||||
|
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
|
|
||||||
|
|
||||||
// Compose zone
|
|
||||||
implementation(platform("androidx.compose:compose-bom:2023.04.01"))
|
|
||||||
|
|
||||||
implementation("androidx.compose.material3:material3:1.1.1")
|
|
||||||
// implementation("androidx.compose.material:material:1.4.3")
|
|
||||||
implementation("androidx.compose.ui:ui:1.4.3")
|
|
||||||
|
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
|
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
|
|
||||||
|
|
||||||
implementation("androidx.compose.material3:material3-window-size-class:1.1.1")
|
|
||||||
|
|
||||||
implementation("androidx.activity:activity-compose:1.7.2")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
|
|
||||||
|
|
||||||
implementation("androidx.compose.runtime:runtime-saveable:1.6.0-alpha02")
|
|
||||||
// end of Compose zone
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "3ebd234270e36902d3d461af38664869",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "accounts",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, PRIMARY KEY(`userId`))",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '3ebd234270e36902d3d461af38664869')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "6a940f719e8dd56ea5c196c152f1e536",
|
||||||
|
"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, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "firstName",
|
||||||
|
"columnName": "firstName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastName",
|
||||||
|
"columnName": "lastName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isOnline",
|
||||||
|
"columnName": "isOnline",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isOnlineMobile",
|
||||||
|
"columnName": "isOnlineMobile",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "onlineAppId",
|
||||||
|
"columnName": "onlineAppId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastSeen",
|
||||||
|
"columnName": "lastSeen",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastSeenStatus",
|
||||||
|
"columnName": "lastSeenStatus",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "birthday",
|
||||||
|
"columnName": "birthday",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo50",
|
||||||
|
"columnName": "photo50",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo100",
|
||||||
|
"columnName": "photo100",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo200",
|
||||||
|
"columnName": "photo200",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "groups",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "screenName",
|
||||||
|
"columnName": "screenName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo50",
|
||||||
|
"columnName": "photo50",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo100",
|
||||||
|
"columnName": "photo100",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo200",
|
||||||
|
"columnName": "photo200",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "membersCount",
|
||||||
|
"columnName": "membersCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "messages",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "text",
|
||||||
|
"columnName": "text",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isOut",
|
||||||
|
"columnName": "isOut",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "peerId",
|
||||||
|
"columnName": "peerId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fromId",
|
||||||
|
"columnName": "fromId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "date",
|
||||||
|
"columnName": "date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "randomId",
|
||||||
|
"columnName": "randomId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "action",
|
||||||
|
"columnName": "action",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "actionMemberId",
|
||||||
|
"columnName": "actionMemberId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "actionText",
|
||||||
|
"columnName": "actionText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "actionConversationMessageId",
|
||||||
|
"columnName": "actionConversationMessageId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "actionMessage",
|
||||||
|
"columnName": "actionMessage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updateTime",
|
||||||
|
"columnName": "updateTime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "important",
|
||||||
|
"columnName": "important",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "forwardIds",
|
||||||
|
"columnName": "forwardIds",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "replyMessageId",
|
||||||
|
"columnName": "replyMessageId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "geoType",
|
||||||
|
"columnName": "geoType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "conversations",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localId",
|
||||||
|
"columnName": "localId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ownerId",
|
||||||
|
"columnName": "ownerId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo50",
|
||||||
|
"columnName": "photo50",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo100",
|
||||||
|
"columnName": "photo100",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "photo200",
|
||||||
|
"columnName": "photo200",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isPhantom",
|
||||||
|
"columnName": "isPhantom",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastConversationMessageId",
|
||||||
|
"columnName": "lastConversationMessageId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReadCmId",
|
||||||
|
"columnName": "inReadCmId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "outReadCmId",
|
||||||
|
"columnName": "outReadCmId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inRead",
|
||||||
|
"columnName": "inRead",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "outRead",
|
||||||
|
"columnName": "outRead",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastMessageId",
|
||||||
|
"columnName": "lastMessageId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unreadCount",
|
||||||
|
"columnName": "unreadCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "membersCount",
|
||||||
|
"columnName": "membersCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "canChangePin",
|
||||||
|
"columnName": "canChangePin",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "canChangeInfo",
|
||||||
|
"columnName": "canChangeInfo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "majorId",
|
||||||
|
"columnName": "majorId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "minorId",
|
||||||
|
"columnName": "minorId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinnedMessageId",
|
||||||
|
"columnName": "pinnedMessageId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "peerType",
|
||||||
|
"columnName": "peerType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6a940f719e8dd56ea5c196c152f1e536')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.meloda.app.fast.tests
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertHasClickAction
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LoginSignInTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun signInButtonIsClickable() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
// LogoScreen(onAction = {})
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag(testTag = "Sign in button").assertHasClickAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun signInButtonTriggersSignInAction() {
|
||||||
|
var signInClicked = true
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
// com.meloda.fast.auth.login.presentation.LogoScreen(
|
||||||
|
// onAction = { action ->
|
||||||
|
// when (action) {
|
||||||
|
// UiAction.NextClicked -> {
|
||||||
|
// signInClicked = true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// else -> Unit
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("Sign in button").performClick()
|
||||||
|
|
||||||
|
assert(signInClicked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -1,6 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#F44336</color>
|
||||||
<string name="app_name">Fast Dev</string>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name" translatable="false">Fast Debug</string>
|
||||||
|
</resources>
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,6 +1,5 @@
|
|||||||
<?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:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
@@ -9,29 +8,22 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".common.AppGlobal"
|
android:name="com.meloda.app.fast.common.AppGlobal"
|
||||||
android:allowBackup="false"
|
android:allowBackup="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:fullBackupOnly="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:supportsRtl="false"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:testOnly="false"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme">
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
tools:ignore="DataExtractionRules"
|
|
||||||
tools:replace="android:allowBackup"
|
|
||||||
tools:targetApi="tiramisu">
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".screens.main.activity.MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.app.shortcuts"
|
|
||||||
android:resource="@xml/shortcuts" />
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@@ -39,10 +31,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".screens.testing.TestActivity" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.LongPollService"
|
android:name=".service.longpolling.LongPollingService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
@@ -53,33 +43,6 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".service.LongPollQSTileService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:icon="@drawable/ic_round_settings_24"
|
|
||||||
android:label="Open settings"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".service.MyCustomControlService"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="Fast"
|
|
||||||
android:permission="android.permission.BIND_CONTROLS">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.controls.ControlsProviderService" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
@@ -89,15 +52,6 @@
|
|||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".receiver.StopLongPollServiceReceiver"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.meloda.fast.receiver.ACTION_STOP" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package com.meloda.app.fast
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.meloda.app.fast.common.UiText
|
||||||
|
import com.meloda.app.fast.common.extensions.isSdkAtLeast
|
||||||
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
|
import com.meloda.app.fast.datastore.SettingsKeys
|
||||||
|
import com.meloda.app.fast.datastore.UserSettings
|
||||||
|
import com.meloda.app.fast.datastore.model.ThemeConfig
|
||||||
|
import com.meloda.app.fast.designsystem.AppTheme
|
||||||
|
import com.meloda.app.fast.designsystem.CheckPermission
|
||||||
|
import com.meloda.app.fast.designsystem.LocalTheme
|
||||||
|
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||||
|
import com.meloda.app.fast.designsystem.RequestPermission
|
||||||
|
import com.meloda.app.fast.model.MainScreenState
|
||||||
|
import com.meloda.app.fast.service.OnlineService
|
||||||
|
import com.meloda.app.fast.service.longpolling.LongPollingService
|
||||||
|
import org.koin.compose.KoinContext
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
createNotificationChannels()
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
KoinContext {
|
||||||
|
val userSettings: UserSettings = koinInject()
|
||||||
|
LaunchedEffect(true) {
|
||||||
|
userSettings.updateUsingDarkTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
val isLongPollInBackground by userSettings.longPollBackground.collectAsStateWithLifecycle()
|
||||||
|
toggleLongPollService(true, isLongPollInBackground)
|
||||||
|
|
||||||
|
val isOnline by userSettings.online.collectAsStateWithLifecycle()
|
||||||
|
LifecycleResumeEffect(isOnline) {
|
||||||
|
toggleOnlineService(isOnline)
|
||||||
|
|
||||||
|
onPauseOrDispose {
|
||||||
|
toggleOnlineService(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val theme by userSettings.theme.collectAsStateWithLifecycle()
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalTheme provides ThemeConfig(
|
||||||
|
usingDarkStyle = theme.usingDarkStyle,
|
||||||
|
usingDynamicColors = theme.usingDynamicColors,
|
||||||
|
selectedColorScheme = theme.selectedColorScheme,
|
||||||
|
usingAmoledBackground = theme.usingAmoledBackground,
|
||||||
|
usingBlur = theme.usingBlur,
|
||||||
|
multiline = theme.multiline
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AppTheme(
|
||||||
|
useDarkTheme = LocalTheme.current.usingDarkStyle,
|
||||||
|
useDynamicColors = LocalTheme.current.usingDynamicColors,
|
||||||
|
selectedColorScheme = LocalTheme.current.selectedColorScheme,
|
||||||
|
useAmoledBackground = LocalTheme.current.usingAmoledBackground,
|
||||||
|
) {
|
||||||
|
RootGraph()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannels() {
|
||||||
|
isSdkAtLeast(Build.VERSION_CODES.O) {
|
||||||
|
val dialogsName = "Dialogs"
|
||||||
|
val dialogsDescriptionText = "Channel for dialogs notifications"
|
||||||
|
val dialogsImportance = NotificationManager.IMPORTANCE_HIGH
|
||||||
|
val dialogsChannel =
|
||||||
|
NotificationChannel("simple_notifications", dialogsName, dialogsImportance).apply {
|
||||||
|
description = dialogsDescriptionText
|
||||||
|
}
|
||||||
|
|
||||||
|
val longPollName = "Long Polling"
|
||||||
|
val longPollDescriptionText = "Channel for long polling service"
|
||||||
|
val longPollImportance = NotificationManager.IMPORTANCE_NONE
|
||||||
|
val longPollChannel =
|
||||||
|
NotificationChannel("long_polling", longPollName, longPollImportance).apply {
|
||||||
|
description = longPollDescriptionText
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannels(listOf(dialogsChannel, longPollChannel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleLongPollService(
|
||||||
|
enable: Boolean,
|
||||||
|
asForeground: Boolean = SettingsController.getBoolean(
|
||||||
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||||
|
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (enable) {
|
||||||
|
val longPollIntent = Intent(this, LongPollingService::class.java)
|
||||||
|
|
||||||
|
if (asForeground) {
|
||||||
|
ContextCompat.startForegroundService(this, longPollIntent)
|
||||||
|
} else {
|
||||||
|
startService(longPollIntent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopService(Intent(this, LongPollingService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleOnlineService(enable: Boolean) {
|
||||||
|
if (enable) {
|
||||||
|
startService(Intent(this, OnlineService::class.java))
|
||||||
|
} else {
|
||||||
|
stopService(Intent(this, OnlineService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopServices() {
|
||||||
|
toggleOnlineService(enable = false)
|
||||||
|
|
||||||
|
val asForeground = SettingsController.getBoolean(
|
||||||
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||||
|
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!asForeground) {
|
||||||
|
toggleLongPollService(enable = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setNewLanguage(newLanguage: String) {
|
||||||
|
val appLocales = AppCompatDelegate.getApplicationLocales()
|
||||||
|
if (newLanguage.isEmpty()) {
|
||||||
|
if (!appLocales.isEmpty) {
|
||||||
|
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!appLocales.toLanguageTags().startsWith(newLanguage)) {
|
||||||
|
val newLocale = LocaleListCompat.forLanguageTags(newLanguage)
|
||||||
|
AppCompatDelegate.setApplicationLocales(newLocale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
stopServices()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun NotificationsPermissionChecker(
|
||||||
|
screenState: MainScreenState,
|
||||||
|
viewModel: MainViewModel
|
||||||
|
) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||||
|
|
||||||
|
val permission =
|
||||||
|
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
|
||||||
|
if (screenState.isNeedToOpenAppPermissions) {
|
||||||
|
viewModel.onAppPermissionsOpened()
|
||||||
|
|
||||||
|
LocalContext.current.apply {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", this.packageName, null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenState.isNeedToRequestNotifications) {
|
||||||
|
RequestPermission(permission = permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isNeedToCheckNotificationsPermission by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||||
|
SettingsController.getBoolean(
|
||||||
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||||
|
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNeedToCheckNotificationsPermission) {
|
||||||
|
CheckPermission(
|
||||||
|
showRationale = {
|
||||||
|
MaterialDialog(
|
||||||
|
title = UiText.Resource(UiR.string.warning),
|
||||||
|
text = UiText.Simple("The application will not be able to work properly without permission to send notifications."),
|
||||||
|
confirmText = UiText.Simple("Grant"),
|
||||||
|
confirmAction = {
|
||||||
|
viewModel.onRequestNotificationsPermissionClicked(true)
|
||||||
|
},
|
||||||
|
cancelText = UiText.Resource(UiR.string.cancel),
|
||||||
|
cancelAction = viewModel::onNotificationsAlertNegativeClicked,
|
||||||
|
onDismissAction = viewModel::onNotificationsAlertNegativeClicked,
|
||||||
|
buttonsInvokeDismiss = false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDenied = {
|
||||||
|
MaterialDialog(
|
||||||
|
title = UiText.Resource(UiR.string.warning),
|
||||||
|
text = UiText.Simple("The application needs permission to send notifications to update messages and other information."),
|
||||||
|
confirmText = UiText.Simple("Grant"),
|
||||||
|
confirmAction = {
|
||||||
|
viewModel.onRequestNotificationsPermissionClicked(false)
|
||||||
|
},
|
||||||
|
cancelText = UiText.Resource(UiR.string.cancel),
|
||||||
|
onDismissAction = {},
|
||||||
|
buttonsInvokeDismiss = false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
permission = permission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.meloda.app.fast
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideIn
|
||||||
|
import androidx.compose.animation.slideOut
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
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.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.navigation
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.meloda.app.fast.conversations.navigation.Conversations
|
||||||
|
import com.meloda.app.fast.conversations.navigation.conversationsRoute
|
||||||
|
import com.meloda.app.fast.friends.navigation.Friends
|
||||||
|
import com.meloda.app.fast.friends.navigation.friendsRoute
|
||||||
|
import com.meloda.app.fast.model.BaseError
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object MainGraph
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object Main
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object Profile
|
||||||
|
|
||||||
|
data class BottomNavigationItem(
|
||||||
|
val titleResId: Int,
|
||||||
|
val selectedIconResId: Int,
|
||||||
|
val unselectedIconResId: Int,
|
||||||
|
val route: Any,
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun NavGraphBuilder.mainScreen(
|
||||||
|
onError: (BaseError) -> Unit,
|
||||||
|
onNavigateToSettings: () -> Unit,
|
||||||
|
onNavigateToMessagesHistory: (conversationId: Int) -> Unit,
|
||||||
|
) {
|
||||||
|
val items = listOf(
|
||||||
|
BottomNavigationItem(
|
||||||
|
titleResId = UiR.string.title_friends,
|
||||||
|
selectedIconResId = UiR.drawable.baseline_people_alt_24,
|
||||||
|
unselectedIconResId = UiR.drawable.outline_people_alt_24,
|
||||||
|
route = Friends,
|
||||||
|
),
|
||||||
|
BottomNavigationItem(
|
||||||
|
titleResId = UiR.string.title_conversations,
|
||||||
|
selectedIconResId = UiR.drawable.baseline_chat_24,
|
||||||
|
unselectedIconResId = UiR.drawable.outline_chat_24,
|
||||||
|
route = Conversations
|
||||||
|
),
|
||||||
|
BottomNavigationItem(
|
||||||
|
titleResId = UiR.string.title_profile,
|
||||||
|
selectedIconResId = UiR.drawable.baseline_account_circle_24,
|
||||||
|
unselectedIconResId = UiR.drawable.outline_account_circle_24,
|
||||||
|
route = Profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val routes = items.map(BottomNavigationItem::route)
|
||||||
|
|
||||||
|
composable<Main> {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
var selectedItemIndex by rememberSaveable {
|
||||||
|
mutableIntStateOf(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isBottomBarVisible by rememberSaveable {
|
||||||
|
mutableStateOf(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
bottomBar = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isBottomBarVisible,
|
||||||
|
enter = slideIn { IntOffset(0, 400) },
|
||||||
|
exit = slideOut { IntOffset(0, 400) }
|
||||||
|
) {
|
||||||
|
NavigationBar {
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = selectedItemIndex == index,
|
||||||
|
onClick = {
|
||||||
|
if (selectedItemIndex != index) {
|
||||||
|
val currentRoute = routes[selectedItemIndex]
|
||||||
|
|
||||||
|
selectedItemIndex = index
|
||||||
|
navController.navigate(item.route) {
|
||||||
|
popUpTo(route = currentRoute) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (selectedItemIndex == index) item.selectedIconResId
|
||||||
|
else item.unselectedIconResId
|
||||||
|
),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
alwaysShowLabel = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = padding.calculateBottomPadding())
|
||||||
|
) {
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = MainGraph,
|
||||||
|
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||||
|
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||||
|
) {
|
||||||
|
navigation<MainGraph>(startDestination = Conversations) {
|
||||||
|
friendsRoute(
|
||||||
|
onError = onError,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
conversationsRoute(
|
||||||
|
onError = onError,
|
||||||
|
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||||
|
navController = navController,
|
||||||
|
onListScrollingUp = { isScrolling ->
|
||||||
|
// isBottomBarVisible = isScrolling
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
composable<Profile> {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(id = UiR.string.title_profile))
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onNavigateToSettings) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Settings,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package com.meloda.app.fast
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.meloda.app.fast.common.UserConfig
|
||||||
|
import com.meloda.app.fast.common.extensions.setValue
|
||||||
|
import com.meloda.app.fast.common.extensions.updateValue
|
||||||
|
import com.meloda.app.fast.data.db.AccountsRepository
|
||||||
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
|
import com.meloda.app.fast.datastore.SettingsKeys
|
||||||
|
import com.meloda.app.fast.datastore.UserSettings
|
||||||
|
import com.meloda.app.fast.model.BaseError
|
||||||
|
import com.meloda.app.fast.model.LongPollState
|
||||||
|
import com.meloda.app.fast.model.MainScreenState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
interface MainViewModel {
|
||||||
|
|
||||||
|
val screenState: StateFlow<MainScreenState>
|
||||||
|
|
||||||
|
val longPollState: StateFlow<LongPollState>
|
||||||
|
val startOnlineService: StateFlow<Boolean>
|
||||||
|
|
||||||
|
fun useDynamicColorsChanged(use: Boolean)
|
||||||
|
|
||||||
|
fun useDarkThemeChanged(use: Boolean)
|
||||||
|
|
||||||
|
fun onRequestNotificationsPermissionClicked(fromRationale: Boolean)
|
||||||
|
fun onNotificationsAlertNegativeClicked()
|
||||||
|
|
||||||
|
fun onNotificationsRequested()
|
||||||
|
|
||||||
|
fun onAppPermissionsOpened()
|
||||||
|
|
||||||
|
fun onError(error: BaseError)
|
||||||
|
|
||||||
|
fun onAuthOpened()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainViewModelImpl(
|
||||||
|
private val accountsRepository: AccountsRepository,
|
||||||
|
private val userSettings: UserSettings
|
||||||
|
) : MainViewModel, ViewModel() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
|
||||||
|
|
||||||
|
override val longPollState = MutableStateFlow(
|
||||||
|
if (SettingsController.getBoolean(
|
||||||
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||||
|
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
LongPollState.ForegroundService
|
||||||
|
} else {
|
||||||
|
LongPollState.DefaultService
|
||||||
|
}
|
||||||
|
)
|
||||||
|
override val startOnlineService = MutableStateFlow(
|
||||||
|
SettingsController.getBoolean(
|
||||||
|
SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||||
|
SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun useDynamicColorsChanged(use: Boolean) {
|
||||||
|
screenState.updateValue(screenState.value.copy(useDynamicColors = use))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun useDarkThemeChanged(use: Boolean) {
|
||||||
|
screenState.updateValue(screenState.value.copy(useDarkTheme = use))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestNotificationsPermissionClicked(fromRationale: Boolean) {
|
||||||
|
screenState.setValue { old ->
|
||||||
|
if (fromRationale) {
|
||||||
|
old.copy(isNeedToOpenAppPermissions = true)
|
||||||
|
} else {
|
||||||
|
old.copy(isNeedToRequestNotifications = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationsAlertNegativeClicked() {
|
||||||
|
SettingsController.edit {
|
||||||
|
putBoolean(
|
||||||
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
userSettings.setLongPollBackground(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationsRequested() {
|
||||||
|
screenState.setValue { old -> old.copy(isNeedToRequestNotifications = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAppPermissionsOpened() {
|
||||||
|
screenState.setValue { old -> old.copy(isNeedToOpenAppPermissions = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(error: BaseError) {
|
||||||
|
when (error) {
|
||||||
|
BaseError.SessionExpired -> {
|
||||||
|
screenState.setValue { old -> old.copy(isNeedToOpenAuth = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthOpened() {
|
||||||
|
screenState.setValue { old -> old.copy(isNeedToOpenAuth = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAccounts() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val accounts = accountsRepository.getAccounts()
|
||||||
|
|
||||||
|
Log.d("MainViewModel", "initUserConfig: accounts: $accounts")
|
||||||
|
|
||||||
|
if (accounts.isNotEmpty()) {
|
||||||
|
val currentAccount = accounts.find { it.userId == UserConfig.currentUserId }
|
||||||
|
if (currentAccount != null) {
|
||||||
|
UserConfig.apply {
|
||||||
|
this.userId = currentAccount.userId
|
||||||
|
this.accessToken = currentAccount.accessToken
|
||||||
|
this.fastToken = currentAccount.fastToken
|
||||||
|
this.trustedHash = currentAccount.trustedHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
accounts = accounts,
|
||||||
|
accountsLoaded = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.meloda.app.fast
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.meloda.app.fast.auth.AuthGraph
|
||||||
|
import com.meloda.app.fast.auth.authNavGraph
|
||||||
|
import com.meloda.app.fast.auth.navigateToAuth
|
||||||
|
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsRoute
|
||||||
|
import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials
|
||||||
|
import com.meloda.app.fast.common.UserConfig
|
||||||
|
import com.meloda.app.fast.languagepicker.navigation.languagePickerRoute
|
||||||
|
import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker
|
||||||
|
import com.meloda.app.fast.messageshistory.navigation.messagesHistoryRoute
|
||||||
|
import com.meloda.app.fast.messageshistory.navigation.navigateToMessagesHistory
|
||||||
|
import com.meloda.app.fast.settings.presentation.navigateToSettings
|
||||||
|
import com.meloda.app.fast.settings.presentation.settingsRoute
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RootGraph(navController: NavHostController = rememberNavController()) {
|
||||||
|
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
||||||
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
if (screenState.isNeedToOpenAuth) {
|
||||||
|
viewModel.onAuthOpened()
|
||||||
|
navController.navigateToAuth(clearBackStack = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenState.accountsLoaded) {
|
||||||
|
val isNeedToShowConversations by remember {
|
||||||
|
derivedStateOf { screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn() }
|
||||||
|
}
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = if (isNeedToShowConversations) Main else AuthGraph,
|
||||||
|
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||||
|
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||||
|
) {
|
||||||
|
authNavGraph(
|
||||||
|
onError = viewModel::onError,
|
||||||
|
onNavigateToMain = navController::navigateToMain,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
mainScreen(
|
||||||
|
onError = viewModel::onError,
|
||||||
|
onNavigateToSettings = navController::navigateToSettings,
|
||||||
|
onNavigateToMessagesHistory = navController::navigateToMessagesHistory
|
||||||
|
)
|
||||||
|
|
||||||
|
messagesHistoryRoute(
|
||||||
|
onError = viewModel::onError,
|
||||||
|
onBack = navController::navigateUp,
|
||||||
|
onNavigateToChatAttachments = navController::navigateToChatMaterials
|
||||||
|
)
|
||||||
|
chatMaterialsRoute(
|
||||||
|
onBack = navController::navigateUp
|
||||||
|
)
|
||||||
|
|
||||||
|
settingsRoute(
|
||||||
|
onError = viewModel::onError,
|
||||||
|
onBack = navController::navigateUp,
|
||||||
|
onNavigateToAuth = { navController.navigateToAuth(true) },
|
||||||
|
onNavigateToLanguagePicker = navController::navigateToLanguagePicker
|
||||||
|
)
|
||||||
|
languagePickerRoute(onBack = navController::navigateUp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationsPermissionChecker(
|
||||||
|
screenState = screenState,
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavController.navigateToMain() {
|
||||||
|
this.navigate(Main) {
|
||||||
|
popUpTo(0) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.meloda.app.fast.common
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.ImageLoaderFactory
|
||||||
|
import com.meloda.app.fast.common.di.applicationModule
|
||||||
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.android.ext.koin.androidLogger
|
||||||
|
import org.koin.core.context.GlobalContext.startKoin
|
||||||
|
|
||||||
|
class AppGlobal : Application(), ImageLoaderFactory {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
SettingsController.init(preferences)
|
||||||
|
UserConfig.init(preferences)
|
||||||
|
|
||||||
|
initKoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initKoin() {
|
||||||
|
startKoin {
|
||||||
|
androidLogger()
|
||||||
|
androidContext(this@AppGlobal)
|
||||||
|
modules(applicationModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(): ImageLoader = get()
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.meloda.app.fast.common.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.meloda.app.fast.MainViewModelImpl
|
||||||
|
import com.meloda.app.fast.auth.authModule
|
||||||
|
import com.meloda.app.fast.conversations.di.conversationsModule
|
||||||
|
import com.meloda.app.fast.data.di.dataModule
|
||||||
|
import com.meloda.app.fast.friends.di.friendsModule
|
||||||
|
import com.meloda.app.fast.languagepicker.di.languagePickerModule
|
||||||
|
import com.meloda.app.fast.messageshistory.di.messagesHistoryModule
|
||||||
|
import com.meloda.app.fast.photoviewer.di.photoViewModule
|
||||||
|
import com.meloda.app.fast.profile.di.profileModule
|
||||||
|
import com.meloda.app.fast.service.longpolling.di.longPollModule
|
||||||
|
import com.meloda.app.fast.settings.di.settingsModule
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.core.qualifier.qualifier
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val applicationModule = module {
|
||||||
|
includes(dataModule)
|
||||||
|
includes(
|
||||||
|
authModule,
|
||||||
|
conversationsModule,
|
||||||
|
settingsModule,
|
||||||
|
messagesHistoryModule,
|
||||||
|
photoViewModule,
|
||||||
|
languagePickerModule,
|
||||||
|
longPollModule,
|
||||||
|
friendsModule,
|
||||||
|
profileModule
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
|
||||||
|
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
|
||||||
|
singleOf(PreferenceManager::getDefaultSharedPreferences)
|
||||||
|
single<Resources> { androidContext().resources }
|
||||||
|
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||||
|
|
||||||
|
viewModelOf(::MainViewModelImpl) {
|
||||||
|
qualifier = qualifier("main")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.meloda.app.fast.model
|
||||||
|
|
||||||
|
sealed class LongPollState {
|
||||||
|
data object ForegroundService : LongPollState()
|
||||||
|
data object DefaultService : LongPollState()
|
||||||
|
data object Stop : LongPollState()
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.meloda.app.fast.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.meloda.app.fast.model.database.AccountEntity
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class MainScreenState(
|
||||||
|
val accounts: List<AccountEntity>,
|
||||||
|
val accountsLoaded: Boolean,
|
||||||
|
val useDarkTheme: Boolean,
|
||||||
|
val useDynamicColors: Boolean,
|
||||||
|
val isNeedToRequestNotifications: Boolean,
|
||||||
|
val isNeedToOpenAppPermissions: Boolean,
|
||||||
|
val isNeedToOpenAuth: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val EMPTY: MainScreenState = MainScreenState(
|
||||||
|
accounts = emptyList(),
|
||||||
|
accountsLoaded = false,
|
||||||
|
|
||||||
|
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||||
|
useDarkTheme = false,
|
||||||
|
useDynamicColors = false,
|
||||||
|
isNeedToRequestNotifications = false,
|
||||||
|
isNeedToOpenAppPermissions = false,
|
||||||
|
isNeedToOpenAuth = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.meloda.app.fast.model
|
||||||
|
|
||||||
|
sealed class ServicesState {
|
||||||
|
data object Started : ServicesState()
|
||||||
|
data object Stopped : ServicesState()
|
||||||
|
data object Unknown : ServicesState()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.receiver
|
package com.meloda.app.fast.receiver
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.meloda.app.fast.service
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import com.meloda.app.fast.common.UserConfig
|
||||||
|
import com.meloda.app.fast.common.extensions.createTimerFlow
|
||||||
|
import com.meloda.app.fast.data.api.account.AccountUseCase
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
class OnlineService : Service() {
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
|
||||||
|
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
Log.d(TAG, "error: $throwable")
|
||||||
|
throwable.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val coroutineContext: CoroutineContext
|
||||||
|
get() = Dispatchers.Default + job + exceptionHandler
|
||||||
|
|
||||||
|
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||||
|
|
||||||
|
private val useCase: AccountUseCase by inject()
|
||||||
|
|
||||||
|
private var timerJob: Job? = null
|
||||||
|
private var onlineJob: Job? = null
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
Log.d(STATE_TAG, "onBind: intent: $intent")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (startId > 1) return START_STICKY
|
||||||
|
|
||||||
|
Log.d(STATE_TAG, "onStartCommand: flags: $flags; startId: $startId\ninstance: $this")
|
||||||
|
|
||||||
|
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||||
|
// if (AppGlobal.preferences.getBoolean(
|
||||||
|
// SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||||
|
// SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
|
||||||
|
// )
|
||||||
|
// ) {
|
||||||
|
// createTimer()
|
||||||
|
// }
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTimer() {
|
||||||
|
timerJob = createTimerFlow(
|
||||||
|
isNeedToEndCondition = { false },
|
||||||
|
onStartAction = ::setOnline,
|
||||||
|
onTickAction = ::setOnline,
|
||||||
|
interval = 5.minutes
|
||||||
|
).launchIn(coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOnline() {
|
||||||
|
if (onlineJob != null) return
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||||
|
// if (!AppGlobal.preferences.getBoolean(
|
||||||
|
// SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||||
|
// SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
|
||||||
|
// )
|
||||||
|
// ) return
|
||||||
|
|
||||||
|
Log.d(TAG, "setOnline()")
|
||||||
|
|
||||||
|
onlineJob = coroutineScope.launch {
|
||||||
|
val token = UserConfig.fastToken ?: UserConfig.accessToken
|
||||||
|
|
||||||
|
if (token.isBlank()) {
|
||||||
|
Log.d(TAG, "setOnline: token is empty")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = useCase.setOnline(
|
||||||
|
voip = false,
|
||||||
|
accessToken = token
|
||||||
|
)
|
||||||
|
Log.d(TAG, "setOnline: response: $response")
|
||||||
|
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setOffline() {
|
||||||
|
Log.d(TAG, "setOffline()")
|
||||||
|
|
||||||
|
val response = useCase.setOffline(
|
||||||
|
accessToken = UserConfig.accessToken
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.d(TAG, "setOffline: response: $response")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLowMemory() {
|
||||||
|
Log.d(STATE_TAG, "onLowMemory")
|
||||||
|
super.onLowMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Log.d(STATE_TAG, "onDestroy")
|
||||||
|
|
||||||
|
timerJob?.cancel("OnlineService destroyed")
|
||||||
|
onlineJob?.cancel("OnlineService destroyed")
|
||||||
|
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "OnlineService"
|
||||||
|
private const val STATE_TAG = "OnlineServiceState"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package com.meloda.app.fast.service.longpolling
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import com.conena.nanokt.android.app.stopForegroundCompat
|
||||||
|
import com.meloda.app.fast.common.UserConfig
|
||||||
|
import com.meloda.app.fast.common.VkConstants
|
||||||
|
import com.meloda.app.fast.common.extensions.listenValue
|
||||||
|
import com.meloda.app.fast.data.LongPollUpdatesParser
|
||||||
|
import com.meloda.app.fast.data.LongPollUseCase
|
||||||
|
import com.meloda.app.fast.data.processState
|
||||||
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
|
import com.meloda.app.fast.datastore.SettingsKeys
|
||||||
|
import com.meloda.app.fast.model.api.data.LongPollUpdates
|
||||||
|
import com.meloda.app.fast.model.api.data.VkLongPollData
|
||||||
|
import com.meloda.app.fast.util.NotificationsUtils
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
class LongPollingService : Service() {
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
|
||||||
|
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
Log.e(TAG, "error: $throwable")
|
||||||
|
|
||||||
|
if (throwable !is NoAccessTokenException) {
|
||||||
|
throwable.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val coroutineContext: CoroutineContext
|
||||||
|
get() = Dispatchers.IO + job + exceptionHandler
|
||||||
|
|
||||||
|
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||||
|
|
||||||
|
private val longPollUseCase: LongPollUseCase by inject()
|
||||||
|
private val updatesParser: LongPollUpdatesParser by inject()
|
||||||
|
private val preferences: SharedPreferences by inject()
|
||||||
|
|
||||||
|
private var currentJob: Job? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
Log.d(STATE_TAG, "onCreate()")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
Log.d(STATE_TAG, "onBind: intent: $intent")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (startId > 1) return START_STICKY
|
||||||
|
|
||||||
|
val asForeground = preferences.getBoolean(
|
||||||
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||||
|
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
STATE_TAG,
|
||||||
|
"onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId;\ninstance: $this"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||||
|
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||||
|
.putExtra(Settings.EXTRA_CHANNEL_ID, "long_polling")
|
||||||
|
} else {
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
.setData(Uri.fromParts("package", packageName, null))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
val openCategorySettingsPendingIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
1,
|
||||||
|
openCategorySettingsIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
if (asForeground) {
|
||||||
|
val notification =
|
||||||
|
NotificationsUtils.createNotification(
|
||||||
|
context = this,
|
||||||
|
title = "LongPoll",
|
||||||
|
contentText = "нажмите, чтобы убрать уведомление",
|
||||||
|
notRemovable = false,
|
||||||
|
channelId = "long_polling",
|
||||||
|
priority = NotificationsUtils.NotificationPriority.Low,
|
||||||
|
category = NotificationCompat.CATEGORY_SERVICE,
|
||||||
|
customNotificationId = NOTIFICATION_ID,
|
||||||
|
contentIntent = openCategorySettingsPendingIntent
|
||||||
|
).build()
|
||||||
|
|
||||||
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
} else {
|
||||||
|
stopForegroundCompat(ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPolling(): Job {
|
||||||
|
if (job.isCompleted || job.isCancelled) {
|
||||||
|
Log.d(STATE_TAG, "job is completed or cancelled")
|
||||||
|
throw Exception("Job is over")
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(STATE_TAG, "job started")
|
||||||
|
|
||||||
|
return coroutineScope.launch {
|
||||||
|
if (UserConfig.accessToken.isEmpty()) {
|
||||||
|
throw NoAccessTokenException
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverInfo = getServerInfo()
|
||||||
|
?: throw LongPollException(message = "bad VK response (server info)")
|
||||||
|
|
||||||
|
var lastUpdatesResponse: LongPollUpdates? = getUpdatesResponse(serverInfo)
|
||||||
|
?: throw LongPollException(message = "initiation error: bad VK response (last updates)")
|
||||||
|
|
||||||
|
var failCount = 0
|
||||||
|
|
||||||
|
while (job.isActive) {
|
||||||
|
if (lastUpdatesResponse == null) {
|
||||||
|
failCount++
|
||||||
|
serverInfo = getServerInfo()
|
||||||
|
?: throw LongPollException(message = "failed retrieving server info after error: bad VK response (server info #2)")
|
||||||
|
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
when (lastUpdatesResponse.failed) {
|
||||||
|
1 -> {
|
||||||
|
val newTs = lastUpdatesResponse.ts ?: kotlin.run {
|
||||||
|
failCount++
|
||||||
|
serverInfo.ts
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
|
||||||
|
}
|
||||||
|
|
||||||
|
2, 3 -> {
|
||||||
|
serverInfo = getServerInfo()
|
||||||
|
?: throw LongPollException(
|
||||||
|
message = "failed retrieving server info after error: bad VK response (server info #3)"
|
||||||
|
)
|
||||||
|
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val newTs = lastUpdatesResponse.ts
|
||||||
|
|
||||||
|
if (newTs == null) {
|
||||||
|
failCount++
|
||||||
|
} else {
|
||||||
|
val updates = lastUpdatesResponse.updates
|
||||||
|
|
||||||
|
if (updates == null) {
|
||||||
|
failCount++
|
||||||
|
} else {
|
||||||
|
updates.forEach(updatesParser::parseNextUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getServerInfo(): VkLongPollData? = suspendCoroutine {
|
||||||
|
longPollUseCase.getLongPollServer(
|
||||||
|
needPts = true,
|
||||||
|
version = VkConstants.LP_VERSION
|
||||||
|
).listenValue(coroutineScope) { state ->
|
||||||
|
state.processState(
|
||||||
|
success = { response ->
|
||||||
|
Log.d(TAG, "getServerInfo: serverInfoResponse: $response")
|
||||||
|
it.resume(response)
|
||||||
|
},
|
||||||
|
error = { error ->
|
||||||
|
Log.e(TAG, "getServerInfo: $error")
|
||||||
|
it.resume(null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getUpdatesResponse(
|
||||||
|
server: VkLongPollData
|
||||||
|
): LongPollUpdates? = suspendCoroutine {
|
||||||
|
longPollUseCase.getLongPollUpdates(
|
||||||
|
serverUrl = "https://${server.server}",
|
||||||
|
key = server.key,
|
||||||
|
ts = server.ts,
|
||||||
|
wait = 25,
|
||||||
|
mode = 2 or 8 or 32 or 64 or 128,
|
||||||
|
version = VkConstants.LP_VERSION
|
||||||
|
).listenValue(coroutineScope) { state ->
|
||||||
|
state.processState(
|
||||||
|
success = { response ->
|
||||||
|
Log.d(TAG, "lastUpdateResponse: $response")
|
||||||
|
it.resume(response)
|
||||||
|
},
|
||||||
|
error = { error ->
|
||||||
|
Log.d(TAG, "getUpdatesResponse: error: $error")
|
||||||
|
it.resume(null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Log.d(STATE_TAG, "onDestroy")
|
||||||
|
try {
|
||||||
|
SettingsController.edit {
|
||||||
|
putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true)
|
||||||
|
}
|
||||||
|
job.cancel()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLowMemory() {
|
||||||
|
Log.d(STATE_TAG, "onLowMemory")
|
||||||
|
super.onLowMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "LongPollTask"
|
||||||
|
|
||||||
|
private const val STATE_TAG = "LongPollServiceState"
|
||||||
|
|
||||||
|
const val KEY_LONG_POLL_WAS_DESTROYED = "long_poll_was_destroyed"
|
||||||
|
|
||||||
|
private const val NOTIFICATION_ID = 1001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LongPollException(override val message: String) : Throwable()
|
||||||
|
private data object NoAccessTokenException : Throwable()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.meloda.app.fast.service.longpolling.di
|
||||||
|
|
||||||
|
import com.meloda.app.fast.data.LongPollUpdatesParser
|
||||||
|
import com.meloda.app.fast.data.LongPollUseCase
|
||||||
|
import com.meloda.app.fast.data.LongPollUseCaseImpl
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.bind
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val longPollModule = module {
|
||||||
|
singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class
|
||||||
|
singleOf(::LongPollUpdatesParser)
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.meloda.fast.util
|
package com.meloda.app.fast.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
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 com.meloda.fast.R
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
object NotificationsUtils {
|
object NotificationsUtils {
|
||||||
|
|
||||||
@@ -27,7 +27,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)
|
||||||
@@ -69,5 +69,4 @@ object NotificationsUtils {
|
|||||||
enum class NotificationPriority(val value: Int) {
|
enum class NotificationPriority(val value: Int) {
|
||||||
Default(0), Low(-1), Min(-2), High(1), Max(2)
|
Default(0), Low(-1), Min(-2), High(1), Max(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.meloda.fast.api
|
|
||||||
|
|
||||||
enum class ApiEvent(val value: Int) {
|
|
||||||
MessageSetFlags(2),
|
|
||||||
MessageClearFlags(3),
|
|
||||||
MessageNew(4),
|
|
||||||
MessageEdit(5),
|
|
||||||
MessageReadIncoming(6),
|
|
||||||
MessageReadOutgoing(7),
|
|
||||||
MessagesDeleted(13),
|
|
||||||
PinUnpinConversation(20),
|
|
||||||
PrivateTyping(61),
|
|
||||||
ChatTyping(62),
|
|
||||||
OneMoreTyping(63),
|
|
||||||
VoiceRecording(64),
|
|
||||||
PhotoUploading(65),
|
|
||||||
VideoUploading(66),
|
|
||||||
FileUploading(67),
|
|
||||||
UnreadCountUpdate(80)
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun parse(value: Int) = values().firstOrNull { it.value == value }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.meloda.fast.api
|
|
||||||
|
|
||||||
object ApiExtensions {
|
|
||||||
|
|
||||||
val Boolean.intString get() = (if (this) 1 else 0).toString()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.meloda.fast.api.base
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import okio.IOException
|
|
||||||
|
|
||||||
open class ApiError(
|
|
||||||
@SerializedName("error", alternate = ["error_code"])
|
|
||||||
val error: String? = null,
|
|
||||||
@SerializedName("error_msg", alternate = ["error_description"])
|
|
||||||
open val errorMessage: String? = null,
|
|
||||||
@SerializedName("error_type")
|
|
||||||
val errorType: String? = null,
|
|
||||||
val throwable: Throwable? = null
|
|
||||||
) : IOException() {
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return Gson().toJson(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.meloda.fast.api.base
|
|
||||||
|
|
||||||
data class ApiResponse<T>(
|
|
||||||
val error: ApiError? = null,
|
|
||||||
val response: T? = null
|
|
||||||
) {
|
|
||||||
val isSuccessful get() = error == null && response != null
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.meloda.fast.api.base
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
|
||||||
import okio.IOException
|
|
||||||
|
|
||||||
class AttachmentClassNameIsEmptyException(attachment: VkAttachment) :
|
|
||||||
IOException(
|
|
||||||
"attachment ${attachment.javaClass.name} does not have declared field \"className\""
|
|
||||||
)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.meloda.fast.api.longpoll
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.VkGroup
|
|
||||||
import com.meloda.fast.api.model.VkMessage
|
|
||||||
import com.meloda.fast.api.model.VkUser
|
|
||||||
|
|
||||||
sealed class LongPollEvent {
|
|
||||||
|
|
||||||
data class VkMessageNewEvent(
|
|
||||||
val message: VkMessage,
|
|
||||||
val profiles: HashMap<Int, VkUser>,
|
|
||||||
val groups: HashMap<Int, VkGroup>,
|
|
||||||
) : LongPollEvent()
|
|
||||||
|
|
||||||
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent()
|
|
||||||
|
|
||||||
data class VkMessageReadIncomingEvent(
|
|
||||||
val peerId: Int,
|
|
||||||
val messageId: Int,
|
|
||||||
val unreadCount: Int,
|
|
||||||
) : LongPollEvent()
|
|
||||||
|
|
||||||
data class VkMessageReadOutgoingEvent(
|
|
||||||
val peerId: Int,
|
|
||||||
val messageId: Int,
|
|
||||||
val unreadCount: Int,
|
|
||||||
) : LongPollEvent()
|
|
||||||
|
|
||||||
data class VkConversationPinStateChangedEvent(
|
|
||||||
val peerId: Int,
|
|
||||||
val majorId: Int,
|
|
||||||
) : LongPollEvent()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
package com.meloda.fast.api.longpoll
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.meloda.fast.api.ApiEvent
|
|
||||||
import com.meloda.fast.api.VKConstants
|
|
||||||
import com.meloda.fast.api.model.VkGroup
|
|
||||||
import com.meloda.fast.api.model.VkUser
|
|
||||||
import com.meloda.fast.api.network.ApiAnswer
|
|
||||||
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
|
|
||||||
import com.meloda.fast.base.viewmodel.VkEventCallback
|
|
||||||
import com.meloda.fast.data.messages.MessagesRepository
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate")
|
|
||||||
class LongPollUpdatesParser(private val messagesRepository: MessagesRepository) : CoroutineScope {
|
|
||||||
|
|
||||||
private val job = SupervisorJob()
|
|
||||||
|
|
||||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
|
||||||
Log.d("LongPollUpdatesParser", "error: $throwable")
|
|
||||||
throwable.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext
|
|
||||||
get() = Dispatchers.Default + job + exceptionHandler
|
|
||||||
|
|
||||||
private val listenersMap: MutableMap<ApiEvent, MutableCollection<VkEventCallback<*>>> =
|
|
||||||
mutableMapOf()
|
|
||||||
|
|
||||||
fun parseNextUpdate(event: JsonArray) {
|
|
||||||
val eventId = event[0].asInt
|
|
||||||
val eventType: ApiEvent? = ApiEvent.parse(eventId)
|
|
||||||
|
|
||||||
if (eventType == null) {
|
|
||||||
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (eventType) {
|
|
||||||
ApiEvent.MessageSetFlags -> parseMessageSetFlags(eventType, event)
|
|
||||||
ApiEvent.MessageClearFlags -> parseMessageClearFlags(eventType, event)
|
|
||||||
ApiEvent.MessageNew -> parseMessageNew(eventType, event)
|
|
||||||
ApiEvent.MessageEdit -> parseMessageEdit(eventType, event)
|
|
||||||
ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event)
|
|
||||||
ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event)
|
|
||||||
ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event)
|
|
||||||
ApiEvent.PinUnpinConversation -> parseConversationPinStateChanged(eventType, event)
|
|
||||||
ApiEvent.PrivateTyping -> onNewEvent(eventType, event)
|
|
||||||
ApiEvent.ChatTyping -> onNewEvent(eventType, event)
|
|
||||||
ApiEvent.OneMoreTyping -> onNewEvent(eventType, event)
|
|
||||||
ApiEvent.VoiceRecording -> onNewEvent(eventType, event)
|
|
||||||
ApiEvent.PhotoUploading -> onNewEvent(eventType, event)
|
|
||||||
ApiEvent.VideoUploading -> onNewEvent(eventType, event)
|
|
||||||
ApiEvent.FileUploading -> onNewEvent(eventType, event)
|
|
||||||
ApiEvent.UnreadCountUpdate -> onNewEvent(eventType, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onNewEvent(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseConversationPinStateChanged(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val peerId = event[1].asInt
|
|
||||||
val majorId = event[2].asInt
|
|
||||||
|
|
||||||
launch {
|
|
||||||
listenersMap[ApiEvent.PinUnpinConversation]?.let { listeners ->
|
|
||||||
listeners.forEach { vkEventCallback ->
|
|
||||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>)
|
|
||||||
.onEvent(
|
|
||||||
LongPollEvent.VkConversationPinStateChangedEvent(
|
|
||||||
peerId = peerId,
|
|
||||||
majorId = majorId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
val messageId = event[1].asInt
|
|
||||||
|
|
||||||
launch {
|
|
||||||
val newMessageEvent: LongPollEvent.VkMessageNewEvent =
|
|
||||||
loadNormalMessage(
|
|
||||||
eventType,
|
|
||||||
messageId
|
|
||||||
)
|
|
||||||
|
|
||||||
listenersMap[ApiEvent.MessageNew]?.let {
|
|
||||||
it.map { vkEventCallback ->
|
|
||||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
|
|
||||||
.onEvent(newMessageEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
val messageId = event[1].asInt
|
|
||||||
|
|
||||||
launch {
|
|
||||||
val editedMessageEvent: LongPollEvent.VkMessageEditEvent =
|
|
||||||
loadNormalMessage(
|
|
||||||
eventType,
|
|
||||||
messageId
|
|
||||||
)
|
|
||||||
|
|
||||||
listenersMap[ApiEvent.MessageEdit]?.let {
|
|
||||||
it.map { vkEventCallback ->
|
|
||||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
|
|
||||||
.onEvent(editedMessageEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
val peerId = event[1].asInt
|
|
||||||
val messageId = event[2].asInt
|
|
||||||
val unreadCount = event[3].asInt
|
|
||||||
|
|
||||||
launch {
|
|
||||||
listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners ->
|
|
||||||
listeners.map { vkEventCallback ->
|
|
||||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
|
|
||||||
.onEvent(
|
|
||||||
LongPollEvent.VkMessageReadIncomingEvent(
|
|
||||||
peerId = peerId,
|
|
||||||
messageId = messageId,
|
|
||||||
unreadCount = unreadCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
val peerId = event[1].asInt
|
|
||||||
val messageId = event[2].asInt
|
|
||||||
val unreadCount = event[3].asInt
|
|
||||||
|
|
||||||
launch {
|
|
||||||
listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners ->
|
|
||||||
listeners.map { vkEventCallback ->
|
|
||||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
|
|
||||||
.onEvent(
|
|
||||||
LongPollEvent.VkMessageReadOutgoingEvent(
|
|
||||||
peerId = peerId,
|
|
||||||
messageId = messageId,
|
|
||||||
unreadCount = unreadCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
|
|
||||||
coroutineScope {
|
|
||||||
suspendCoroutine {
|
|
||||||
launch {
|
|
||||||
val normalMessageResponse = messagesRepository.getById(
|
|
||||||
MessagesGetByIdRequest(
|
|
||||||
messagesIds = listOf(messageId),
|
|
||||||
extended = true,
|
|
||||||
fields = VKConstants.ALL_FIELDS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (normalMessageResponse.isError()) {
|
|
||||||
normalMessageResponse.error.throwable?.run { throw this }
|
|
||||||
}
|
|
||||||
|
|
||||||
val messagesResponse =
|
|
||||||
(normalMessageResponse as? ApiAnswer.Success)?.data?.response
|
|
||||||
?: return@launch
|
|
||||||
|
|
||||||
val messagesList = messagesResponse.items
|
|
||||||
if (messagesList.isEmpty()) return@launch
|
|
||||||
|
|
||||||
val normalMessage = messagesList[0].asVkMessage()
|
|
||||||
messagesRepository.store(listOf(normalMessage))
|
|
||||||
|
|
||||||
val profiles = hashMapOf<Int, VkUser>()
|
|
||||||
messagesResponse.profiles?.forEach { baseUser ->
|
|
||||||
baseUser.mapToDomain().let { user -> profiles[user.id] = user }
|
|
||||||
}
|
|
||||||
|
|
||||||
val groups = hashMapOf<Int, VkGroup>()
|
|
||||||
messagesResponse.groups?.forEach { baseGroup ->
|
|
||||||
baseGroup.mapToDomain().let { group -> groups[group.id] = group }
|
|
||||||
}
|
|
||||||
|
|
||||||
val resumeValue: LongPollEvent? = when (eventType) {
|
|
||||||
ApiEvent.MessageNew ->
|
|
||||||
LongPollEvent.VkMessageNewEvent(
|
|
||||||
normalMessage,
|
|
||||||
profiles,
|
|
||||||
groups
|
|
||||||
)
|
|
||||||
ApiEvent.MessageEdit -> LongPollEvent.VkMessageEditEvent(normalMessage)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeValue?.let { value -> it.resume(value as T) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun <T : Any> registerListener(eventType: ApiEvent, listener: VkEventCallback<T>) {
|
|
||||||
listenersMap.let { map ->
|
|
||||||
map[eventType] = (map[eventType] ?: mutableListOf()).also {
|
|
||||||
it.add(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onConversationPinStateChanged(listener: VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) {
|
|
||||||
registerListener(ApiEvent.PinUnpinConversation, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) {
|
|
||||||
onConversationPinStateChanged(assembleEventCallback(block))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
|
|
||||||
registerListener(ApiEvent.MessageReadIncoming, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
|
|
||||||
onMessageIncomingRead(assembleEventCallback(block))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
|
|
||||||
registerListener(ApiEvent.MessageReadOutgoing, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
|
|
||||||
onMessageOutgoingRead(assembleEventCallback(block))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
|
|
||||||
registerListener(ApiEvent.MessageNew, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
|
|
||||||
onNewMessage(assembleEventCallback(block))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
|
|
||||||
registerListener(ApiEvent.MessageEdit, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
|
|
||||||
onMessageEdited(assembleEventCallback(block))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearListeners() {
|
|
||||||
listenersMap.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal inline fun <R : Any> assembleEventCallback(
|
|
||||||
crossinline block: (R) -> Unit,
|
|
||||||
): VkEventCallback<R> {
|
|
||||||
return VkEventCallback { event -> block.invoke(event) }
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.meloda.fast.api.model
|
|
||||||
|
|
||||||
sealed class ActionState {
|
|
||||||
object Phantom : ActionState()
|
|
||||||
object CallInProgress : ActionState()
|
|
||||||
object None : ActionState()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun parse(isPhantom: Boolean, isCallInProgress: Boolean): ActionState {
|
|
||||||
return when {
|
|
||||||
isPhantom -> Phantom
|
|
||||||
isCallInProgress -> CallInProgress
|
|
||||||
else -> None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.meloda.fast.api.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
sealed class ConversationPeerType : Parcelable {
|
|
||||||
object User : ConversationPeerType()
|
|
||||||
object Group : ConversationPeerType()
|
|
||||||
object Chat : ConversationPeerType()
|
|
||||||
|
|
||||||
fun isUser() = this == User
|
|
||||||
fun isGroup() = this == Group
|
|
||||||
fun isChat() = this == Chat
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun parse(type: String): ConversationPeerType {
|
|
||||||
return when (type) {
|
|
||||||
"user" -> User
|
|
||||||
"group" -> Group
|
|
||||||
else -> Chat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.meloda.fast.api.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkChatMember(
|
|
||||||
val memberId: Int,
|
|
||||||
val invitedBy: Int,
|
|
||||||
val joinDate: Int,
|
|
||||||
val isAdmin: Boolean,
|
|
||||||
val isOwner: Boolean,
|
|
||||||
val canKick: Boolean
|
|
||||||
) : Parcelable
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.meloda.fast.api.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Entity(tableName = "groups")
|
|
||||||
@Parcelize
|
|
||||||
data class VkGroup(
|
|
||||||
@PrimaryKey(autoGenerate = false)
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
val screenName: String,
|
|
||||||
val photo200: String?,
|
|
||||||
val membersCount: Int?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
override fun toString() = name.trim()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
package com.meloda.fast.api.model
|
|
||||||
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.Ignore
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import com.meloda.fast.api.UserConfig
|
|
||||||
import com.meloda.fast.api.VKConstants
|
|
||||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
|
||||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
|
||||||
import com.meloda.fast.api.model.domain.VkConversationDomain
|
|
||||||
import com.meloda.fast.model.SelectableItem
|
|
||||||
import com.meloda.fast.util.TimeUtils
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
// TODO: 05.08.2023, Danil Nikolaev: create other class for storing in database
|
|
||||||
@Entity(tableName = "messages")
|
|
||||||
@Parcelize
|
|
||||||
data class VkMessage constructor(
|
|
||||||
@PrimaryKey(autoGenerate = false)
|
|
||||||
var id: Int,
|
|
||||||
var text: String? = null,
|
|
||||||
val isOut: Boolean,
|
|
||||||
val peerId: Int,
|
|
||||||
val fromId: Int,
|
|
||||||
val date: Int,
|
|
||||||
val randomId: Int,
|
|
||||||
val action: String? = null,
|
|
||||||
val actionMemberId: Int? = null,
|
|
||||||
val actionText: String? = null,
|
|
||||||
val actionConversationMessageId: Int? = null,
|
|
||||||
val actionMessage: String? = null,
|
|
||||||
|
|
||||||
var updateTime: Int? = null,
|
|
||||||
|
|
||||||
var important: Boolean = false,
|
|
||||||
|
|
||||||
var forwards: List<VkMessage>? = null,
|
|
||||||
var attachments: List<VkAttachment>? = null,
|
|
||||||
var replyMessage: VkMessage? = null,
|
|
||||||
|
|
||||||
val geo: BaseVkMessage.Geo? = null,
|
|
||||||
) : SelectableItem() {
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var user: VkUser? = null
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var group: VkGroup? = null
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var actionUser: VkUser? = null
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var actionGroup: VkGroup? = null
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var state: State = State.Sent
|
|
||||||
|
|
||||||
fun isPeerChat() = peerId > 2_000_000_000
|
|
||||||
|
|
||||||
fun isUser() = fromId > 0
|
|
||||||
|
|
||||||
fun isGroup() = fromId < 0
|
|
||||||
|
|
||||||
fun isRead(conversation: VkConversationDomain) =
|
|
||||||
if (isOut) {
|
|
||||||
conversation.outRead - id >= 0
|
|
||||||
} else {
|
|
||||||
conversation.inRead - id >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPreparedAction(): Action? {
|
|
||||||
if (action == null) return null
|
|
||||||
return Action.parse(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canEdit() =
|
|
||||||
fromId == UserConfig.userId &&
|
|
||||||
(attachments == null ||
|
|
||||||
!VKConstants.restrictedToEditAttachments.contains(
|
|
||||||
requireNotNull(attachments).first().javaClass
|
|
||||||
)) &&
|
|
||||||
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.OneDayInSeconds)
|
|
||||||
|
|
||||||
fun hasAttachments(): Boolean = !attachments.isNullOrEmpty()
|
|
||||||
|
|
||||||
fun hasReply(): Boolean = replyMessage != null
|
|
||||||
|
|
||||||
fun hasForwards(): Boolean = !forwards.isNullOrEmpty()
|
|
||||||
|
|
||||||
fun hasGeo(): Boolean = geo != null
|
|
||||||
|
|
||||||
fun isUpdated(): Boolean = updateTime != null && requireNotNull(updateTime) > 0
|
|
||||||
|
|
||||||
fun isSending(): Boolean = state == State.Sending
|
|
||||||
|
|
||||||
fun isError(): Boolean = state == State.Error
|
|
||||||
|
|
||||||
fun isSent(): Boolean = state == State.Sent
|
|
||||||
|
|
||||||
enum class Action(val value: String) {
|
|
||||||
CHAT_CREATE("chat_create"),
|
|
||||||
CHAT_PHOTO_UPDATE("chat_photo_update"),
|
|
||||||
CHAT_PHOTO_REMOVE("chat_photo_remove"),
|
|
||||||
CHAT_TITLE_UPDATE("chat_title_update"),
|
|
||||||
CHAT_PIN_MESSAGE("chat_pin_message"),
|
|
||||||
CHAT_UNPIN_MESSAGE("chat_unpin_message"),
|
|
||||||
CHAT_INVITE_USER("chat_invite_user"),
|
|
||||||
CHAT_INVITE_USER_BY_LINK("chat_invite_user_by_link"),
|
|
||||||
CHAT_KICK_USER("chat_kick_user"),
|
|
||||||
CHAT_SCREENSHOT("chat_screenshot"),
|
|
||||||
|
|
||||||
CHAT_INVITE_USER_BY_CALL("chat_invite_user_by_call"),
|
|
||||||
CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link"),
|
|
||||||
CHAT_STYLE_UPDATE("conversation_style_update");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun parse(value: String?): Action? = values().firstOrNull { it.value == value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class State {
|
|
||||||
Sending, Sent, Error
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.meloda.fast.api.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Entity(tableName = "users")
|
|
||||||
@Parcelize
|
|
||||||
data class VkUser(
|
|
||||||
@PrimaryKey(autoGenerate = false)
|
|
||||||
val id: Int,
|
|
||||||
val firstName: String,
|
|
||||||
val lastName: String,
|
|
||||||
val online: Boolean,
|
|
||||||
val photo200: String?,
|
|
||||||
val lastSeen: Int?,
|
|
||||||
val lastSeenStatus: String?,
|
|
||||||
val birthday: String?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
override fun toString() = fullName
|
|
||||||
|
|
||||||
val fullName get() = "$firstName $lastName".trim()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
open class VkAttachment : Parcelable {
|
|
||||||
|
|
||||||
open fun asString(withAccessKey: Boolean = true) = ""
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import com.meloda.fast.api.VkUtils
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkAudio(
|
|
||||||
val id: Int,
|
|
||||||
val ownerId: Int,
|
|
||||||
val title: String,
|
|
||||||
val artist: String,
|
|
||||||
val url: String,
|
|
||||||
val duration: Int,
|
|
||||||
val accessKey: String?
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
|
|
||||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
|
||||||
attachmentClass = this::class.java,
|
|
||||||
id = id,
|
|
||||||
ownerId = ownerId,
|
|
||||||
withAccessKey = withAccessKey,
|
|
||||||
accessKey = accessKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkCall(
|
|
||||||
val initiatorId: Int,
|
|
||||||
val receiverId: Int,
|
|
||||||
val state: String,
|
|
||||||
val time: Int,
|
|
||||||
val duration: Int,
|
|
||||||
val isVideo: Boolean
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkCurator(
|
|
||||||
val id: Int,
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkEvent(
|
|
||||||
val id: Int
|
|
||||||
) : VkAttachment()
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import com.meloda.fast.api.VkUtils
|
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkFile
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkFile(
|
|
||||||
val id: Int,
|
|
||||||
val ownerId: Int,
|
|
||||||
val title: String,
|
|
||||||
val ext: String,
|
|
||||||
val size: Int,
|
|
||||||
val url: String,
|
|
||||||
val accessKey: String?,
|
|
||||||
val preview: BaseVkFile.Preview?
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
|
|
||||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
|
||||||
attachmentClass = this::class.java,
|
|
||||||
id = id,
|
|
||||||
ownerId = ownerId,
|
|
||||||
withAccessKey = withAccessKey,
|
|
||||||
accessKey = accessKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkGift(
|
|
||||||
val id: Int,
|
|
||||||
val thumb256: String?,
|
|
||||||
val thumb96: String?,
|
|
||||||
val thumb48: String
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkGraffiti(
|
|
||||||
val id: Int,
|
|
||||||
val ownerId: Int,
|
|
||||||
val url: String,
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val accessKey: String
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkGroupCall(
|
|
||||||
val initiatorId: Int
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkLink(
|
|
||||||
val url: String,
|
|
||||||
val title: String?,
|
|
||||||
val caption: String?,
|
|
||||||
val photo: VkPhoto?,
|
|
||||||
val target: String?,
|
|
||||||
val isFavorite: Boolean
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkMiniApp(
|
|
||||||
val link: String
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkPoll(
|
|
||||||
val id: Int
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkSticker
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkSticker(
|
|
||||||
val id: Int,
|
|
||||||
val productId: Int,
|
|
||||||
val images: List<BaseVkSticker.Image>,
|
|
||||||
val backgroundImages: List<BaseVkSticker.Image>
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
|
|
||||||
fun urlForSize(size: Int): String? {
|
|
||||||
for (image in images) {
|
|
||||||
if (image.width == size) return image.url
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkStory(
|
|
||||||
val id: Int,
|
|
||||||
val ownerId: Int,
|
|
||||||
val date: Int,
|
|
||||||
val photo: VkPhoto?
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
fun isFromUser() = ownerId > 0
|
|
||||||
|
|
||||||
fun isFromGroup() = ownerId < 0
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkVoiceMessage(
|
|
||||||
val id: Int,
|
|
||||||
val ownerId: Int,
|
|
||||||
val duration: Int,
|
|
||||||
val waveform: List<Int>,
|
|
||||||
val linkOgg: String,
|
|
||||||
val linkMp3: String,
|
|
||||||
val accessKey: String,
|
|
||||||
val transcriptState: String?,
|
|
||||||
val transcript: String?
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkWall(
|
|
||||||
val id: Int,
|
|
||||||
val fromId: Int,
|
|
||||||
val toId: Int,
|
|
||||||
val date: Int,
|
|
||||||
val text: String,
|
|
||||||
val attachments: List<BaseVkAttachmentItem>?,
|
|
||||||
val comments: Int?,
|
|
||||||
val likes: Int?,
|
|
||||||
val reposts: Int?,
|
|
||||||
val views: Int?,
|
|
||||||
val isFavorite: Boolean,
|
|
||||||
val accessKey: String?
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkWallReply(
|
|
||||||
val id: Int
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VkWidget(
|
|
||||||
val id: Int
|
|
||||||
) : VkAttachment() {
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val className: String = this::class.java.name
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.VkChat
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkChat(
|
|
||||||
val type: String,
|
|
||||||
val title: String,
|
|
||||||
val admin_id: Int,
|
|
||||||
val members_count: Int,
|
|
||||||
val id: Int,
|
|
||||||
val photo_50: String,
|
|
||||||
val photo_100: String,
|
|
||||||
val photo_200: String,
|
|
||||||
val is_default_photo: Boolean,
|
|
||||||
val push_settings: PushSettings
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkChat() = VkChat(
|
|
||||||
type = type,
|
|
||||||
title = title,
|
|
||||||
adminId = admin_id,
|
|
||||||
membersCount = members_count,
|
|
||||||
id = id,
|
|
||||||
photo50 = photo_50,
|
|
||||||
photo100 = photo_100,
|
|
||||||
photo200 = photo_200,
|
|
||||||
isDefaultPhoto = is_default_photo
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class PushSettings(
|
|
||||||
val sound: Int,
|
|
||||||
val disabled_until: Int
|
|
||||||
) : Parcelable
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.VkChatMember
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkChatMember(
|
|
||||||
val member_id: Int,
|
|
||||||
val invited_by: Int,
|
|
||||||
val join_date: Int,
|
|
||||||
val is_admin: Boolean?,
|
|
||||||
val is_owner: Boolean?,
|
|
||||||
val can_kick: Boolean?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkChatMember() = VkChatMember(
|
|
||||||
memberId = member_id,
|
|
||||||
invitedBy = invited_by,
|
|
||||||
joinDate = join_date,
|
|
||||||
isAdmin = is_admin == true,
|
|
||||||
isOwner = is_owner == true,
|
|
||||||
canKick = can_kick == true
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.VkGroup
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkGroup(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
val screen_name: String,
|
|
||||||
val is_closed: Int,
|
|
||||||
val type: String,
|
|
||||||
val is_admin: Int,
|
|
||||||
val is_member: Int,
|
|
||||||
val is_advertiser: Int,
|
|
||||||
val photo_50: String?,
|
|
||||||
val photo_100: String?,
|
|
||||||
val photo_200: String?,
|
|
||||||
val members_count: Int?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun mapToDomain() = VkGroup(
|
|
||||||
id = -id,
|
|
||||||
name = name,
|
|
||||||
screenName = screen_name,
|
|
||||||
photo200 = photo_200,
|
|
||||||
membersCount = members_count
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkLongPoll(
|
|
||||||
val server: String,
|
|
||||||
val key: String,
|
|
||||||
val ts: Int,
|
|
||||||
val pts: Int
|
|
||||||
) : Parcelable
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.VkUtils
|
|
||||||
import com.meloda.fast.api.model.VkMessage
|
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkMessage(
|
|
||||||
val id: Int,
|
|
||||||
val peer_id: Int,
|
|
||||||
val date: Int,
|
|
||||||
val from_id: Int,
|
|
||||||
val out: Int,
|
|
||||||
val text: String,
|
|
||||||
val conversation_message_id: Int,
|
|
||||||
val fwd_messages: List<BaseVkMessage>? = emptyList(),
|
|
||||||
val important: Boolean,
|
|
||||||
val random_id: Int,
|
|
||||||
val attachments: List<BaseVkAttachmentItem> = emptyList(),
|
|
||||||
val is_hidden: Boolean,
|
|
||||||
val payload: String,
|
|
||||||
val geo: Geo?,
|
|
||||||
val action: Action?,
|
|
||||||
val ttl: Int,
|
|
||||||
val reply_message: BaseVkMessage?,
|
|
||||||
val update_time: Int?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkMessage() = VkMessage(
|
|
||||||
id = id,
|
|
||||||
text = text.ifBlank { null },
|
|
||||||
isOut = out == 1,
|
|
||||||
peerId = peer_id,
|
|
||||||
fromId = from_id,
|
|
||||||
date = date,
|
|
||||||
randomId = random_id,
|
|
||||||
action = action?.type,
|
|
||||||
actionMemberId = action?.member_id,
|
|
||||||
actionText = action?.text,
|
|
||||||
actionConversationMessageId = action?.conversation_message_id,
|
|
||||||
actionMessage = action?.message,
|
|
||||||
geo = geo,
|
|
||||||
important = important,
|
|
||||||
updateTime = update_time
|
|
||||||
).also {
|
|
||||||
it.attachments = VkUtils.parseAttachments(attachments)
|
|
||||||
it.forwards = VkUtils.parseForwards(fwd_messages)
|
|
||||||
it.replyMessage = VkUtils.parseReplyMessage(reply_message)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Geo(
|
|
||||||
val type: String,
|
|
||||||
val coordinates: Coordinates,
|
|
||||||
val place: Place
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Coordinates(val latitude: Float, val longitude: Float) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Place(val country: String, val city: String, val title: String) : Parcelable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Action(
|
|
||||||
val type: String,
|
|
||||||
val member_id: Int?,
|
|
||||||
val text: String?,
|
|
||||||
val conversation_message_id: Int?,
|
|
||||||
val message: String?
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.VkUser
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkUser(
|
|
||||||
val id: Int,
|
|
||||||
val first_name: String,
|
|
||||||
val last_name: String,
|
|
||||||
val can_access_closed: Boolean,
|
|
||||||
val is_closed: Boolean,
|
|
||||||
val can_invite_to_chats: Boolean,
|
|
||||||
val sex: Int?,
|
|
||||||
val photo_50: String?,
|
|
||||||
val photo_100: String?,
|
|
||||||
val photo_200: String?,
|
|
||||||
val online: Int?,
|
|
||||||
val online_info: OnlineInfo?,
|
|
||||||
val screen_name: String,
|
|
||||||
val bdate: String?
|
|
||||||
//...other fields
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class OnlineInfo(
|
|
||||||
val visible: Boolean,
|
|
||||||
val status: String,
|
|
||||||
val last_seen: Int?,
|
|
||||||
val is_online: Boolean?,
|
|
||||||
val online_mobile: Boolean?,
|
|
||||||
val app_id: Int?
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
fun mapToDomain() = VkUser(
|
|
||||||
id = id,
|
|
||||||
firstName = first_name,
|
|
||||||
lastName = last_name,
|
|
||||||
online = online == 1,
|
|
||||||
photo200 = photo_200,
|
|
||||||
lastSeen = online_info?.last_seen,
|
|
||||||
lastSeenStatus = online_info?.status,
|
|
||||||
birthday = bdate
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkAttachmentItem(
|
|
||||||
val type: String,
|
|
||||||
val photo: BaseVkPhoto?,
|
|
||||||
val video: BaseVkVideo?,
|
|
||||||
val audio: BaseVkAudio?,
|
|
||||||
@SerializedName("doc")
|
|
||||||
val file: BaseVkFile?,
|
|
||||||
val link: BaseVkLink?,
|
|
||||||
@SerializedName("mini_app")
|
|
||||||
val miniApp: BaseVkMiniApp?,
|
|
||||||
@SerializedName("audio_message")
|
|
||||||
val voiceMessage: BaseVkVoiceMessage?,
|
|
||||||
val sticker: BaseVkSticker?,
|
|
||||||
val gift: BaseVkGift?,
|
|
||||||
val wall: BaseVkWall?,
|
|
||||||
val graffiti: BaseVkGraffiti?,
|
|
||||||
val poll: BaseVkPoll?,
|
|
||||||
@SerializedName("wall_reply")
|
|
||||||
val wallReply: BaseVkWallReply?,
|
|
||||||
val call: BaseVkCall?,
|
|
||||||
@SerializedName("group_call_in_progress")
|
|
||||||
val groupCall: BaseVkGroupCall?,
|
|
||||||
val curator: BaseVkCurator?,
|
|
||||||
val event: BaseVkEvent?,
|
|
||||||
val story: BaseVkStory?,
|
|
||||||
val widget: BaseVkWidget?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun getPreparedType() = AttachmentType.parse(type)
|
|
||||||
|
|
||||||
enum class AttachmentType(var value: String) {
|
|
||||||
Unknown("unknown"),
|
|
||||||
Photo("photo"),
|
|
||||||
Video("video"),
|
|
||||||
Audio("audio"),
|
|
||||||
File("doc"),
|
|
||||||
Link("link"),
|
|
||||||
Voice("audio_message"),
|
|
||||||
MiniApp("mini_app"),
|
|
||||||
Sticker("sticker"),
|
|
||||||
Gift("gift"),
|
|
||||||
Wall("wall"),
|
|
||||||
Graffiti("graffiti"),
|
|
||||||
Poll("poll"),
|
|
||||||
WallReply("wall_reply"),
|
|
||||||
Call("call"),
|
|
||||||
GroupCallInProgress("group_call_in_progress"),
|
|
||||||
Curator("curator"),
|
|
||||||
Event("event"),
|
|
||||||
Story("story"),
|
|
||||||
Widget("widget")
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun parse(value: String): AttachmentType {
|
|
||||||
val parsedValue = values().firstOrNull { it.value == value } ?: Unknown
|
|
||||||
|
|
||||||
if (parsedValue == Unknown) {
|
|
||||||
Log.e("AttachmentType", "Unknown attachment type: $value")
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class BaseVkAttachment : Parcelable
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkAudio
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkAudio(
|
|
||||||
val id: Int,
|
|
||||||
val title: String,
|
|
||||||
val artist: String,
|
|
||||||
val duration: Int,
|
|
||||||
val url: String,
|
|
||||||
val date: Int,
|
|
||||||
val owner_id: Int,
|
|
||||||
val access_key: String?,
|
|
||||||
val is_explicit: Boolean,
|
|
||||||
val is_focus_track: Boolean,
|
|
||||||
val is_licensed: Boolean,
|
|
||||||
val track_code: String,
|
|
||||||
val genre_id: Int,
|
|
||||||
val album: Album,
|
|
||||||
val short_videos_allowed: Boolean,
|
|
||||||
val stories_allowed: Boolean,
|
|
||||||
val stories_cover_allowed: Boolean
|
|
||||||
) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkAudio() = VkAudio(
|
|
||||||
id = id,
|
|
||||||
ownerId = owner_id,
|
|
||||||
title = title,
|
|
||||||
artist = artist,
|
|
||||||
url = url,
|
|
||||||
duration = duration,
|
|
||||||
accessKey = access_key
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Album(
|
|
||||||
val id: Int,
|
|
||||||
val title: String,
|
|
||||||
val owner_id: Int,
|
|
||||||
val access_key: String,
|
|
||||||
val thumb: Thumb
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Thumb(
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val photo_34: String,
|
|
||||||
val photo_68: String,
|
|
||||||
val photo_135: String,
|
|
||||||
val photo_270: String,
|
|
||||||
val photo_300: String,
|
|
||||||
val photo_600: String,
|
|
||||||
val photo_1200: String
|
|
||||||
) : Parcelable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkCall
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkCall(
|
|
||||||
val initiator_id: Int,
|
|
||||||
val receiver_id: Int,
|
|
||||||
val state: String,
|
|
||||||
val time: Int,
|
|
||||||
val duration: Int,
|
|
||||||
val video: Boolean
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkCall() = VkCall(
|
|
||||||
initiatorId = initiator_id,
|
|
||||||
receiverId = receiver_id,
|
|
||||||
state = state,
|
|
||||||
time = time,
|
|
||||||
duration = duration,
|
|
||||||
isVideo = video
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkCurator
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkCurator(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
val description: String,
|
|
||||||
val url: String,
|
|
||||||
val photo: List<Photo>
|
|
||||||
) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkCurator() = VkCurator(
|
|
||||||
id = id
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Photo(
|
|
||||||
val height: Int,
|
|
||||||
val url: String,
|
|
||||||
val width: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.attachments.VkEvent
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkEvent(
|
|
||||||
val button_text: String,
|
|
||||||
val id: Int,
|
|
||||||
val is_favorite: Boolean,
|
|
||||||
val text: String,
|
|
||||||
val address: String,
|
|
||||||
val friends: List<Int> = emptyList(),
|
|
||||||
val member_status: Int,
|
|
||||||
val time: Int
|
|
||||||
) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkEvent() = VkEvent(id = id)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkFile
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkFile(
|
|
||||||
val id: Int,
|
|
||||||
val owner_id: Int,
|
|
||||||
val title: String,
|
|
||||||
val size: Int,
|
|
||||||
val ext: String,
|
|
||||||
val date: Int,
|
|
||||||
val type: Int,
|
|
||||||
val url: String,
|
|
||||||
val preview: Preview?,
|
|
||||||
val ic_licensed: Int,
|
|
||||||
val access_key: String?,
|
|
||||||
val web_preview_url: String?
|
|
||||||
) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkFile() = VkFile(
|
|
||||||
id = id,
|
|
||||||
ownerId = owner_id,
|
|
||||||
title = title,
|
|
||||||
ext = ext,
|
|
||||||
url = url,
|
|
||||||
size = size,
|
|
||||||
accessKey = access_key,
|
|
||||||
preview = preview
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Preview(
|
|
||||||
val photo: Photo?,
|
|
||||||
val video: Video?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Photo(val sizes: List<Size>) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Size(
|
|
||||||
val height: Int,
|
|
||||||
val width: Int,
|
|
||||||
val type: String,
|
|
||||||
val src: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Video(
|
|
||||||
val src: String,
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val file_size: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkGift
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkGift(
|
|
||||||
val id: Int,
|
|
||||||
val thumb_256: String?,
|
|
||||||
val thumb_96: String?,
|
|
||||||
val thumb_48: String
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkGift() = VkGift(
|
|
||||||
id = id,
|
|
||||||
thumb256 = thumb_256,
|
|
||||||
thumb96 = thumb_96,
|
|
||||||
thumb48 = thumb_48
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkGraffiti
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkGraffiti(
|
|
||||||
val id: Int,
|
|
||||||
val owner_id: Int,
|
|
||||||
val url: String,
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val access_key: String
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkGraffiti() = VkGraffiti(
|
|
||||||
id = id,
|
|
||||||
ownerId = owner_id,
|
|
||||||
url = url,
|
|
||||||
width = width,
|
|
||||||
height = height,
|
|
||||||
accessKey = access_key
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkGroupCall
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkGroupCall(
|
|
||||||
val initiator_id: Int,
|
|
||||||
val join_link: String,
|
|
||||||
val participants: Participants
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Participants(
|
|
||||||
val list: List<Int>,
|
|
||||||
val count: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
fun asVkGroupCall() = VkGroupCall(initiatorId = initiator_id)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.attachments.VkLink
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkLink(
|
|
||||||
val url: String,
|
|
||||||
val title: String?,
|
|
||||||
val caption: String?,
|
|
||||||
val photo: BaseVkPhoto?,
|
|
||||||
val target: String?,
|
|
||||||
val is_favorite: Boolean
|
|
||||||
) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkLink() = VkLink(
|
|
||||||
url = url,
|
|
||||||
title = title,
|
|
||||||
caption = caption,
|
|
||||||
photo = photo?.asVkPhoto(),
|
|
||||||
target = target,
|
|
||||||
isFavorite = is_favorite
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.meloda.fast.api.model.attachments.VkMiniApp
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkMiniApp(
|
|
||||||
val title: String,
|
|
||||||
val description: String,
|
|
||||||
val app: App,
|
|
||||||
val images: List<Image>?,
|
|
||||||
val button_text: String
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class App(
|
|
||||||
val type: String,
|
|
||||||
val id: Int,
|
|
||||||
val title: String,
|
|
||||||
@SerializedName("author_owner_id")
|
|
||||||
val authorOwnerId: Int,
|
|
||||||
@SerializedName("are_notifications_enabled")
|
|
||||||
val areNotificationsEnabled: Boolean,
|
|
||||||
@SerializedName("is_favorite")
|
|
||||||
val isFavorite: Boolean,
|
|
||||||
@SerializedName("is_installed")
|
|
||||||
val isInstalled: Boolean,
|
|
||||||
@SerializedName("track_code")
|
|
||||||
val trackCode: String,
|
|
||||||
@SerializedName("share_url")
|
|
||||||
val shareUrl: String,
|
|
||||||
@SerializedName("webview_url")
|
|
||||||
val webViewUrl: String,
|
|
||||||
@SerializedName("hide_tabbar")
|
|
||||||
val hideTabBar: Int,
|
|
||||||
@SerializedName("icon_75")
|
|
||||||
val icon75: String?,
|
|
||||||
@SerializedName("icon_139")
|
|
||||||
val icon139: String?,
|
|
||||||
@SerializedName("icon_150")
|
|
||||||
val icon150: String?,
|
|
||||||
@SerializedName("icon_278")
|
|
||||||
val icon278: String?,
|
|
||||||
@SerializedName("icon_576")
|
|
||||||
val icon576: String?,
|
|
||||||
@SerializedName("open_in_external_browser")
|
|
||||||
val openInExternalBrowser: Boolean,
|
|
||||||
@SerializedName("need_policy_confirmation")
|
|
||||||
val needPolicyConfirmation: Boolean,
|
|
||||||
@SerializedName("is_vkui_internal")
|
|
||||||
val isVkUiInternal: Boolean,
|
|
||||||
@SerializedName("has_vk_connect")
|
|
||||||
val hasVkConnect: Boolean,
|
|
||||||
@SerializedName("need_show_bottom_menu_tooltip_on_close")
|
|
||||||
val needShowBottomMenuTooltipOnClose: Boolean
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Image(
|
|
||||||
val height: Int,
|
|
||||||
val width: Int,
|
|
||||||
val url: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
fun asVkMiniApp() = VkMiniApp(link = app.shareUrl)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkPhoto
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkPhoto(
|
|
||||||
val album_id: Int,
|
|
||||||
val date: Int,
|
|
||||||
val id: Int,
|
|
||||||
val owner_id: Int,
|
|
||||||
val has_tags: Boolean,
|
|
||||||
val access_key: String?,
|
|
||||||
val sizes: List<Size>,
|
|
||||||
val text: String?,
|
|
||||||
val user_id: Int?,
|
|
||||||
val lat: Double?,
|
|
||||||
val long: Double?,
|
|
||||||
val post_id: Int?
|
|
||||||
) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkPhoto() = VkPhoto(
|
|
||||||
albumId = album_id,
|
|
||||||
date = date,
|
|
||||||
id = id,
|
|
||||||
ownerId = owner_id,
|
|
||||||
hasTags = has_tags,
|
|
||||||
accessKey = access_key,
|
|
||||||
sizes = sizes,
|
|
||||||
text = text,
|
|
||||||
userId = user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Size(
|
|
||||||
val height: Int,
|
|
||||||
val width: Int,
|
|
||||||
val type: String,
|
|
||||||
val url: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkPoll
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkPoll(
|
|
||||||
val multiple: Boolean,
|
|
||||||
val id: Int,
|
|
||||||
val votes: Int,
|
|
||||||
val anonymous: Boolean,
|
|
||||||
val closed: Boolean,
|
|
||||||
val end_date: Int,
|
|
||||||
val is_board: Boolean,
|
|
||||||
val can_vote: Boolean,
|
|
||||||
val can_edit: Boolean,
|
|
||||||
val can_report: Boolean,
|
|
||||||
val can_share: Boolean,
|
|
||||||
val created: Int,
|
|
||||||
val owner_id: Int,
|
|
||||||
val question: String,
|
|
||||||
val disable_unvote: Boolean,
|
|
||||||
val friends: List<Friend>?,
|
|
||||||
val embed_hash: String,
|
|
||||||
val answers: List<Answer>,
|
|
||||||
val author_id: Int,
|
|
||||||
val background: Background?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Friend(
|
|
||||||
val id: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Answer(
|
|
||||||
val id: Int,
|
|
||||||
val rate: Double,
|
|
||||||
val text: String,
|
|
||||||
val votes: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Background(
|
|
||||||
val angle: Int,
|
|
||||||
val color: String,
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
val type: String,
|
|
||||||
val points: List<Point>
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Point(
|
|
||||||
val color: String,
|
|
||||||
val position: Double
|
|
||||||
) : Parcelable
|
|
||||||
}
|
|
||||||
|
|
||||||
fun asVkPoll() = VkPoll(id = id)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkSticker
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkSticker(
|
|
||||||
val product_id: Int,
|
|
||||||
val sticker_id: Int,
|
|
||||||
val images: List<Image>,
|
|
||||||
val images_with_background: List<Image>,
|
|
||||||
val animation_url: String?,
|
|
||||||
val animations: List<Animation>?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkSticker() = VkSticker(
|
|
||||||
id = sticker_id,
|
|
||||||
productId = product_id,
|
|
||||||
images = images,
|
|
||||||
backgroundImages = images_with_background
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Image(
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val url: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Animation(
|
|
||||||
val type: String,
|
|
||||||
val url: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkStory
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkStory(
|
|
||||||
val id: Int,
|
|
||||||
val owner_id: Int,
|
|
||||||
val access_key: String,
|
|
||||||
val can_comment: Int,
|
|
||||||
val can_reply: Int,
|
|
||||||
val can_like: Boolean,
|
|
||||||
val can_share: Int,
|
|
||||||
val can_hide: Int,
|
|
||||||
val date: Int,
|
|
||||||
val expires_at: Int,
|
|
||||||
val is_ads: Boolean,
|
|
||||||
val photo: BaseVkPhoto?,
|
|
||||||
val replies: Replies,
|
|
||||||
val is_one_time: Boolean,
|
|
||||||
val track_code: String,
|
|
||||||
val type: String,
|
|
||||||
val views: Int,
|
|
||||||
val likes_count: Int,
|
|
||||||
val reaction_set_id: String,
|
|
||||||
val is_restricted: Boolean,
|
|
||||||
val no_sound: Boolean,
|
|
||||||
val need_mute: Boolean,
|
|
||||||
val mute_reply: Boolean,
|
|
||||||
val can_ask: Int,
|
|
||||||
val can_ask_anonymous: Int,
|
|
||||||
val preloading_enabled: Boolean,
|
|
||||||
val narratives_count: Int,
|
|
||||||
val can_use_in_narrative: Boolean
|
|
||||||
) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkStory() = VkStory(
|
|
||||||
id = id,
|
|
||||||
ownerId = owner_id,
|
|
||||||
date = date,
|
|
||||||
photo = photo?.asVkPhoto()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Replies(
|
|
||||||
val count: Int,
|
|
||||||
val new: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkVideo
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkVideo(
|
|
||||||
val id: Int,
|
|
||||||
val title: String,
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val duration: Int,
|
|
||||||
val date: Int,
|
|
||||||
val comments: Int,
|
|
||||||
val description: String,
|
|
||||||
val player: String,
|
|
||||||
val added: Int,
|
|
||||||
val type: String,
|
|
||||||
val views: Int,
|
|
||||||
val can_comment: Int,
|
|
||||||
val can_edit: Int,
|
|
||||||
val can_like: Int,
|
|
||||||
val can_repost: Int,
|
|
||||||
val can_subscribe: Int,
|
|
||||||
val can_add_to_faves: Int,
|
|
||||||
val can_add: Int,
|
|
||||||
val can_attach_link: Int,
|
|
||||||
val access_key: String?,
|
|
||||||
val owner_id: Int,
|
|
||||||
val ov_id: String,
|
|
||||||
val is_favorite: Boolean,
|
|
||||||
val track_code: String,
|
|
||||||
val image: List<Image>,
|
|
||||||
val first_frame: List<FirstFrame>,
|
|
||||||
val files: File,
|
|
||||||
val timeline_thumbs: TimelineThumbs,
|
|
||||||
val ads: Ads
|
|
||||||
) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkVideo() = VkVideo(
|
|
||||||
id = id,
|
|
||||||
ownerId = owner_id,
|
|
||||||
images = image.map { it.asVideoImage() },
|
|
||||||
firstFrames = first_frame,
|
|
||||||
accessKey = access_key,
|
|
||||||
title = title
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Image(
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val url: String,
|
|
||||||
val with_padding: Int?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVideoImage() = VkVideo.VideoImage(
|
|
||||||
width = width,
|
|
||||||
height = height,
|
|
||||||
url = url,
|
|
||||||
withPadding = with_padding == 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class FirstFrame(
|
|
||||||
val height: Int,
|
|
||||||
val width: Int,
|
|
||||||
val url: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class File(
|
|
||||||
val mp4_240: String?,
|
|
||||||
val mp4_360: String?,
|
|
||||||
val mp4_480: String?,
|
|
||||||
val mp4_720: String?,
|
|
||||||
val mp4_1080: String?,
|
|
||||||
val mp4_1440: String?,
|
|
||||||
val hls: String,
|
|
||||||
val dash_uni: String,
|
|
||||||
val dash_sep: String,
|
|
||||||
val hls_ondemand: String,
|
|
||||||
val dash_ondemand: String,
|
|
||||||
val failover_host: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class TimelineThumbs(
|
|
||||||
val count_per_image: Int,
|
|
||||||
val count_per_row: Int,
|
|
||||||
val count_total: Int,
|
|
||||||
val frame_height: Int,
|
|
||||||
val frame_width: Float,
|
|
||||||
val links: List<String>,
|
|
||||||
val is_uv: Boolean,
|
|
||||||
val frequency: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Ads(
|
|
||||||
val slot_id: Int,
|
|
||||||
val timeout: Int,
|
|
||||||
val can_play: Int,
|
|
||||||
val params: Params,
|
|
||||||
val sections: List<String>,
|
|
||||||
val midroll_percents: List<Float>
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Params(
|
|
||||||
val vk_id: Int,
|
|
||||||
val duration: Int,
|
|
||||||
val video_id: String,
|
|
||||||
val pl: Int,
|
|
||||||
val content_id: String,
|
|
||||||
val lang: Int,
|
|
||||||
val puid1: String,
|
|
||||||
val puid2: Int,
|
|
||||||
val puid3: Int,
|
|
||||||
val puid5: Int,
|
|
||||||
val puid6: Int,
|
|
||||||
val puid7: Int,
|
|
||||||
val puid9: Int,
|
|
||||||
val puid10: Int,
|
|
||||||
val puid12: Int,
|
|
||||||
val puid13: Int,
|
|
||||||
val puid14: Int,
|
|
||||||
val puid15: Int,
|
|
||||||
val puid18: Int,
|
|
||||||
val puid21: Int,
|
|
||||||
val sign: String,
|
|
||||||
val groupId: Int,
|
|
||||||
val vk_catid: Int,
|
|
||||||
val is_xz_video: Int
|
|
||||||
) : Parcelable
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkVoiceMessage
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkVoiceMessage(
|
|
||||||
val id: Int,
|
|
||||||
val owner_id: Int,
|
|
||||||
val duration: Int,
|
|
||||||
val waveform: List<Int>,
|
|
||||||
val link_ogg: String,
|
|
||||||
val link_mp3: String,
|
|
||||||
val access_key: String,
|
|
||||||
val transcript_state: String?,
|
|
||||||
val transcript: String?
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkVoiceMessage() = VkVoiceMessage(
|
|
||||||
id = id,
|
|
||||||
ownerId = owner_id,
|
|
||||||
duration = duration,
|
|
||||||
waveform = waveform,
|
|
||||||
linkOgg = link_ogg,
|
|
||||||
linkMp3 = link_mp3,
|
|
||||||
accessKey = access_key,
|
|
||||||
transcriptState = transcript_state,
|
|
||||||
transcript = transcript
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.attachments.VkWall
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkWall(
|
|
||||||
val id: Int,
|
|
||||||
val from_id: Int,
|
|
||||||
val to_id: Int,
|
|
||||||
val date: Int,
|
|
||||||
val text: String,
|
|
||||||
val attachments: List<BaseVkAttachmentItem>?,
|
|
||||||
val post_source: PostSource?,
|
|
||||||
val comments: Comments?,
|
|
||||||
val likes: Likes?,
|
|
||||||
val reposts: Reposts?,
|
|
||||||
val views: Views?,
|
|
||||||
val is_favorite: Boolean,
|
|
||||||
val donut: Donut?,
|
|
||||||
val access_key: String?,
|
|
||||||
val short_text_rate: Double
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun asVkWall() = VkWall(
|
|
||||||
id = id,
|
|
||||||
fromId = from_id,
|
|
||||||
toId = to_id,
|
|
||||||
date = date,
|
|
||||||
text = text,
|
|
||||||
attachments = attachments,
|
|
||||||
comments = comments?.count,
|
|
||||||
likes = likes?.count,
|
|
||||||
reposts = reposts?.count,
|
|
||||||
views = views?.count,
|
|
||||||
isFavorite = is_favorite,
|
|
||||||
accessKey = access_key
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class PostSource(
|
|
||||||
val type: String,
|
|
||||||
val platform: String
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Comments(
|
|
||||||
val count: Int,
|
|
||||||
val can_post: Int,
|
|
||||||
val groups_can_post: Boolean
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Likes(
|
|
||||||
val count: Int,
|
|
||||||
val user_likes: Int,
|
|
||||||
val can_like: Int,
|
|
||||||
val can_publish: Int,
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Reposts(
|
|
||||||
val count: Int,
|
|
||||||
val user_reposted: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Views(
|
|
||||||
val count: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Donut(
|
|
||||||
val is_donut: Boolean
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.attachments.VkWidget
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkWidget(val id: Int) : BaseVkAttachment() {
|
|
||||||
|
|
||||||
fun asVkWidget() = VkWidget(id)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.data
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.meloda.fast.api.model.VkGroup
|
|
||||||
import com.meloda.fast.api.model.VkMessage
|
|
||||||
import com.meloda.fast.api.model.VkUser
|
|
||||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall
|
|
||||||
import com.meloda.fast.api.model.domain.VkConversationDomain
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class BaseVkConversation(
|
|
||||||
val peer: Peer,
|
|
||||||
val last_message_id: Int,
|
|
||||||
val in_read: Int,
|
|
||||||
val out_read: Int,
|
|
||||||
val in_read_cmid: Int,
|
|
||||||
val out_read_cmid: Int,
|
|
||||||
val sort_id: SortId,
|
|
||||||
val last_conversation_message_id: Int,
|
|
||||||
val is_marked_unread: Boolean,
|
|
||||||
val important: Boolean,
|
|
||||||
val push_settings: PushSettings,
|
|
||||||
val can_write: CanWrite,
|
|
||||||
val can_send_money: Boolean,
|
|
||||||
val can_receive_money: Boolean,
|
|
||||||
val chat_settings: ChatSettings?,
|
|
||||||
val call_in_progress: CallInProgress?,
|
|
||||||
val unread_count: Int?,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Peer(
|
|
||||||
val id: Int,
|
|
||||||
val type: String,
|
|
||||||
val local_id: Int,
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class SortId(
|
|
||||||
val major_id: Int,
|
|
||||||
val minor_id: Int,
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class PushSettings(
|
|
||||||
val disabled_forever: Boolean,
|
|
||||||
val no_sound: Boolean,
|
|
||||||
val disabled_mentions: Boolean,
|
|
||||||
val disabled_mass_mentions: Boolean,
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class CanWrite(
|
|
||||||
val allowed: Boolean,
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ChatSettings(
|
|
||||||
val owner_id: Int,
|
|
||||||
val title: String,
|
|
||||||
val state: String,
|
|
||||||
val acl: Acl,
|
|
||||||
val members_count: Int,
|
|
||||||
val friends_count: Int,
|
|
||||||
val photo: Photo?,
|
|
||||||
val admin_ids: List<Int>,
|
|
||||||
val active_ids: List<Int>,
|
|
||||||
val is_group_channel: Boolean,
|
|
||||||
val is_disappearing: Boolean,
|
|
||||||
val is_service: Boolean,
|
|
||||||
val theme: String?,
|
|
||||||
val pinned_message: BaseVkMessage?,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Acl(
|
|
||||||
val can_change_info: Boolean,
|
|
||||||
val can_change_invite_link: Boolean,
|
|
||||||
val can_change_pin: Boolean,
|
|
||||||
val can_invite: Boolean,
|
|
||||||
val can_promote_users: Boolean,
|
|
||||||
val can_see_invite_link: Boolean,
|
|
||||||
val can_moderate: Boolean,
|
|
||||||
val can_copy_chat: Boolean,
|
|
||||||
val can_call: Boolean,
|
|
||||||
val can_use_mass_mentions: Boolean,
|
|
||||||
val can_change_style: Boolean,
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Photo(
|
|
||||||
val photo_50: String?,
|
|
||||||
val photo_100: String?,
|
|
||||||
val photo_200: String?,
|
|
||||||
val is_default_photo: Boolean,
|
|
||||||
) : Parcelable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class CallInProgress(
|
|
||||||
val participants: BaseVkGroupCall.Participants,
|
|
||||||
val join_link: String,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Participants(
|
|
||||||
val list: List<Int>,
|
|
||||||
val count: Int,
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToDomain(
|
|
||||||
lastMessage: VkMessage? = null,
|
|
||||||
conversationUser: VkUser? = null,
|
|
||||||
conversationGroup: VkGroup? = null,
|
|
||||||
) = VkConversationDomain(
|
|
||||||
id = peer.id,
|
|
||||||
localId = peer.local_id,
|
|
||||||
conversationTitle = chat_settings?.title,
|
|
||||||
conversationPhoto = chat_settings?.photo?.photo_200,
|
|
||||||
type = peer.type,
|
|
||||||
isCallInProgress = call_in_progress != null,
|
|
||||||
isPhantom = chat_settings?.is_disappearing == true,
|
|
||||||
lastConversationMessageId = last_conversation_message_id,
|
|
||||||
inRead = in_read,
|
|
||||||
outRead = out_read,
|
|
||||||
lastMessageId = last_message_id,
|
|
||||||
unreadCount = unread_count ?: 0,
|
|
||||||
membersCount = chat_settings?.members_count,
|
|
||||||
ownerId = chat_settings?.owner_id,
|
|
||||||
majorId = sort_id.major_id,
|
|
||||||
minorId = sort_id.minor_id,
|
|
||||||
canChangePin = chat_settings?.acl?.can_change_pin == true,
|
|
||||||
canChangeInfo = chat_settings?.acl?.can_change_info == true,
|
|
||||||
pinnedMessageId = chat_settings?.pinned_message?.id,
|
|
||||||
inReadCmId = in_read_cmid,
|
|
||||||
outReadCmId = out_read_cmid,
|
|
||||||
).also {
|
|
||||||
it.lastMessage = lastMessage
|
|
||||||
it.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
|
|
||||||
it.conversationUser = conversationUser
|
|
||||||
it.conversationGroup = conversationGroup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.domain
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.Ignore
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import com.meloda.fast.R
|
|
||||||
import com.meloda.fast.api.UserConfig
|
|
||||||
import com.meloda.fast.api.VkUtils
|
|
||||||
import com.meloda.fast.api.model.ActionState
|
|
||||||
import com.meloda.fast.api.model.ConversationPeerType
|
|
||||||
import com.meloda.fast.api.model.VkGroup
|
|
||||||
import com.meloda.fast.api.model.VkMessage
|
|
||||||
import com.meloda.fast.api.model.VkUser
|
|
||||||
import com.meloda.fast.api.model.presentation.VkConversationUi
|
|
||||||
import com.meloda.fast.common.AppGlobal
|
|
||||||
import com.meloda.fast.ext.isFalse
|
|
||||||
import com.meloda.fast.ext.isTrue
|
|
||||||
import com.meloda.fast.ext.orDots
|
|
||||||
import com.meloda.fast.model.base.UiImage
|
|
||||||
import com.meloda.fast.model.base.UiText
|
|
||||||
import com.meloda.fast.model.base.parseString
|
|
||||||
import com.meloda.fast.util.TimeUtils
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
|
||||||
@Entity(tableName = "conversations")
|
|
||||||
@Parcelize
|
|
||||||
data class VkConversationDomain(
|
|
||||||
@PrimaryKey(autoGenerate = false)
|
|
||||||
val id: Int,
|
|
||||||
val localId: Int,
|
|
||||||
val ownerId: Int?,
|
|
||||||
val conversationTitle: String?,
|
|
||||||
val conversationPhoto: String?,
|
|
||||||
val isCallInProgress: Boolean,
|
|
||||||
val isPhantom: Boolean,
|
|
||||||
val lastConversationMessageId: Int,
|
|
||||||
val inReadCmId: Int,
|
|
||||||
val outReadCmId: Int,
|
|
||||||
val inRead: Int,
|
|
||||||
val outRead: Int,
|
|
||||||
val lastMessageId: Int,
|
|
||||||
val unreadCount: Int,
|
|
||||||
val membersCount: Int?,
|
|
||||||
val canChangePin: Boolean,
|
|
||||||
val canChangeInfo: Boolean,
|
|
||||||
val majorId: Int,
|
|
||||||
val minorId: Int,
|
|
||||||
val pinnedMessageId: Int?,
|
|
||||||
val type: String,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var peerType: ConversationPeerType = ConversationPeerType.parse(type)
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var lastMessage: VkMessage? = null
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var pinnedMessage: VkMessage? = null
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var conversationUser: VkUser? = null
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var conversationGroup: VkGroup? = null
|
|
||||||
|
|
||||||
fun isChat() = peerType.isChat()
|
|
||||||
fun isUser() = peerType.isUser()
|
|
||||||
fun isGroup() = peerType.isGroup()
|
|
||||||
|
|
||||||
fun isInUnread() = inRead - lastMessageId < 0
|
|
||||||
fun isOutUnread() = outRead - lastMessageId < 0
|
|
||||||
|
|
||||||
fun isUnread() = isInUnread() || isOutUnread()
|
|
||||||
|
|
||||||
fun isAccount() = id == UserConfig.userId
|
|
||||||
|
|
||||||
fun isPinned() = majorId > 0
|
|
||||||
|
|
||||||
fun extractAvatar(): UiImage {
|
|
||||||
val placeholderImage = UiImage.Resource(R.drawable.ic_account_circle_cut)
|
|
||||||
|
|
||||||
val avatarLink = when {
|
|
||||||
peerType.isUser() -> {
|
|
||||||
if (id == UserConfig.userId) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
conversationUser?.photo200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peerType.isGroup() -> conversationGroup?.photo200
|
|
||||||
peerType.isChat() -> conversationPhoto
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
return avatarLink?.let(UiImage::Url) ?: placeholderImage
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractTitle(): UiText {
|
|
||||||
return when {
|
|
||||||
isAccount() -> UiText.Resource(R.string.favorites)
|
|
||||||
peerType.isChat() -> UiText.Simple(conversationTitle ?: "...")
|
|
||||||
peerType.isUser() -> UiText.Simple(conversationUser?.fullName ?: "...")
|
|
||||||
peerType.isGroup() -> UiText.Simple(conversationGroup?.name ?: "...")
|
|
||||||
else -> UiText.Simple("...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractUnreadCounterText(): String? {
|
|
||||||
if (lastMessage?.isOut.isFalse && !isInUnread()) return null
|
|
||||||
|
|
||||||
return when (unreadCount) {
|
|
||||||
in 1..999 -> unreadCount.toString()
|
|
||||||
0 -> null
|
|
||||||
else -> "%dK".format(unreadCount / 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 07.01.2023, Danil Nikolaev: rewrite
|
|
||||||
fun extractMessage(): String {
|
|
||||||
val actionMessage = VkUtils.getActionConversationText(
|
|
||||||
message = lastMessage,
|
|
||||||
youPrefix = "You",
|
|
||||||
messageUser = lastMessage?.user,
|
|
||||||
messageGroup = lastMessage?.group,
|
|
||||||
action = lastMessage?.getPreparedAction(),
|
|
||||||
actionUser = lastMessage?.actionUser,
|
|
||||||
actionGroup = lastMessage?.actionGroup
|
|
||||||
)
|
|
||||||
|
|
||||||
val attachmentIcon: UiImage? = when {
|
|
||||||
lastMessage?.text == null -> null
|
|
||||||
!lastMessage?.forwards.isNullOrEmpty() -> {
|
|
||||||
if (lastMessage?.forwards?.size == 1) {
|
|
||||||
UiImage.Resource(R.drawable.ic_attachment_forwarded_message)
|
|
||||||
} else {
|
|
||||||
UiImage.Resource(R.drawable.ic_attachment_forwarded_messages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> VkUtils.getAttachmentConversationIcon(lastMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
val attachmentText = (if (attachmentIcon == null) VkUtils.getAttachmentText(
|
|
||||||
message = lastMessage
|
|
||||||
) else null)
|
|
||||||
|
|
||||||
val forwardsMessage = (if (lastMessage?.text == null) VkUtils.getForwardsText(
|
|
||||||
message = lastMessage
|
|
||||||
) else null)
|
|
||||||
|
|
||||||
val messageText = lastMessage?.text?.let(UiText::Simple)
|
|
||||||
|
|
||||||
var prefix = when {
|
|
||||||
actionMessage != null -> ""
|
|
||||||
lastMessage?.isOut.isTrue -> "You: "
|
|
||||||
else ->
|
|
||||||
when {
|
|
||||||
lastMessage?.user != null && lastMessage?.user?.firstName?.isNotBlank().isTrue -> {
|
|
||||||
"${lastMessage?.user?.firstName}: "
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMessage?.group != null && lastMessage?.group?.name?.isNotBlank().isTrue -> {
|
|
||||||
"${lastMessage?.group?.name}: "
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!peerType.isChat() && lastMessage?.isOut.isFalse) || id == UserConfig.userId)
|
|
||||||
prefix = ""
|
|
||||||
|
|
||||||
val finalText =
|
|
||||||
(actionMessage ?: forwardsMessage ?: attachmentText ?: messageText)
|
|
||||||
?.parseString(AppGlobal.Instance)
|
|
||||||
?.let(VkUtils::prepareMessageText)
|
|
||||||
?.let { text -> "$prefix$text" }
|
|
||||||
|
|
||||||
|
|
||||||
return finalText.orDots()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractAttachmentImage(): UiImage? {
|
|
||||||
if (lastMessage?.text == null) return null
|
|
||||||
return VkUtils.getAttachmentConversationIcon(lastMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractReadCondition(): Boolean {
|
|
||||||
return (lastMessage?.isOut.isTrue && isOutUnread()) ||
|
|
||||||
(lastMessage?.isOut.isFalse && isInUnread())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractDate(): String {
|
|
||||||
return TimeUtils.getLocalizedTime(AppGlobal.Instance, (lastMessage?.date ?: -1) * 1000L)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 05.08.2023, Danil Nikolaev: rewrite
|
|
||||||
fun extractBirthday(): Boolean {
|
|
||||||
val birthday = conversationUser?.birthday ?: return false
|
|
||||||
val splitBirthday = birthday.split(".")
|
|
||||||
|
|
||||||
return if (splitBirthday.size > 1) {
|
|
||||||
val birthdayCalendar = Calendar.getInstance().apply {
|
|
||||||
this[Calendar.DAY_OF_MONTH] = splitBirthday.first().toIntOrNull() ?: -1
|
|
||||||
this[Calendar.MONTH] = (splitBirthday[1].toIntOrNull() ?: 0) - 1
|
|
||||||
}
|
|
||||||
val nowCalendar = Calendar.getInstance()
|
|
||||||
|
|
||||||
(nowCalendar[Calendar.DAY_OF_MONTH] == birthdayCalendar[Calendar.DAY_OF_MONTH]
|
|
||||||
&& nowCalendar[Calendar.MONTH] == birthdayCalendar[Calendar.MONTH])
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToPresentation() = VkConversationUi(
|
|
||||||
conversationId = id,
|
|
||||||
lastMessageId = lastMessageId,
|
|
||||||
avatar = extractAvatar(),
|
|
||||||
title = extractTitle(),
|
|
||||||
unreadCount = extractUnreadCounterText(),
|
|
||||||
date = extractDate(),
|
|
||||||
message = extractMessage(),
|
|
||||||
attachmentImage = extractAttachmentImage(),
|
|
||||||
isPinned = majorId > 0,
|
|
||||||
actionState = ActionState.parse(isPhantom, isCallInProgress),
|
|
||||||
isBirthday = extractBirthday(),
|
|
||||||
isUnread = extractReadCondition(),
|
|
||||||
isAccount = isAccount(),
|
|
||||||
isOnline = !isAccount() && conversationUser?.online == true,
|
|
||||||
lastMessage = lastMessage,
|
|
||||||
conversationUser = conversationUser,
|
|
||||||
conversationGroup = conversationGroup,
|
|
||||||
peerType = peerType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.presentation
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.ActionState
|
|
||||||
import com.meloda.fast.api.model.ConversationPeerType
|
|
||||||
import com.meloda.fast.api.model.VkGroup
|
|
||||||
import com.meloda.fast.api.model.VkMessage
|
|
||||||
import com.meloda.fast.api.model.VkUser
|
|
||||||
import com.meloda.fast.model.base.AdapterDiffItem
|
|
||||||
import com.meloda.fast.model.base.UiImage
|
|
||||||
import com.meloda.fast.model.base.UiText
|
|
||||||
|
|
||||||
data class VkConversationUi(
|
|
||||||
val conversationId: Int,
|
|
||||||
val lastMessageId: Int,
|
|
||||||
val avatar: UiImage,
|
|
||||||
val title: UiText,
|
|
||||||
val unreadCount: String?,
|
|
||||||
val date: String,
|
|
||||||
val message: String,
|
|
||||||
val attachmentImage: UiImage?,
|
|
||||||
val isPinned: Boolean,
|
|
||||||
val actionState: ActionState,
|
|
||||||
val isBirthday: Boolean,
|
|
||||||
val isUnread: Boolean,
|
|
||||||
val isAccount: Boolean,
|
|
||||||
val isOnline: Boolean,
|
|
||||||
val lastMessage: VkMessage?,
|
|
||||||
val conversationUser: VkUser?,
|
|
||||||
val conversationGroup: VkGroup?,
|
|
||||||
val peerType: ConversationPeerType,
|
|
||||||
) : AdapterDiffItem {
|
|
||||||
override val id = conversationId
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package com.meloda.fast.api.network
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.meloda.fast.api.base.ApiError
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
object VkErrorCodes {
|
|
||||||
const val UnknownError = 1
|
|
||||||
const val AppDisabled = 2
|
|
||||||
const val UnknownMethod = 3
|
|
||||||
const val InvalidSignature = 4
|
|
||||||
const val UserAuthorizationFailed = 5
|
|
||||||
const val TooManyRequests = 6
|
|
||||||
const val NoRights = 7
|
|
||||||
const val BadRequest = 8
|
|
||||||
const val TooManySimilarActions = 9
|
|
||||||
const val InternalServerError = 10
|
|
||||||
const val InTestMode = 11
|
|
||||||
const val ExecuteCodeCompileError = 12
|
|
||||||
const val ExecuteCodeRuntimeError = 13
|
|
||||||
const val CaptchaNeeded = 14
|
|
||||||
const val AccessDenied = 15
|
|
||||||
const val RequiresRequestsOverHttps = 16
|
|
||||||
const val ValidationRequired = 17
|
|
||||||
const val UserBannedOrDeleted = 18
|
|
||||||
const val ActionProhibited = 20
|
|
||||||
const val ActionAllowedOnlyForStandalone = 21
|
|
||||||
const val MethodOff = 23
|
|
||||||
const val ConfirmationRequired = 24
|
|
||||||
const val ParameterIsNotSpecified = 100
|
|
||||||
const val IncorrectAppId = 101
|
|
||||||
const val OutOfLimits = 103
|
|
||||||
const val IncorrectUserId = 113
|
|
||||||
const val IncorrectTimestamp = 150
|
|
||||||
const val AccessToAlbumDenied = 200
|
|
||||||
const val AccessToAudioDenied = 201
|
|
||||||
const val AccessToGroupDenied = 203
|
|
||||||
const val AlbumIsFull = 300
|
|
||||||
const val ActionDenied = 500
|
|
||||||
const val PermissionDenied = 600
|
|
||||||
const val CannotSendMessageBlackList = 900
|
|
||||||
const val CannotSendMessageGroup = 901
|
|
||||||
const val InvalidDocId = 1150
|
|
||||||
const val InvalidDocTitle = 1152
|
|
||||||
const val AccessToDocDenied = 1153
|
|
||||||
|
|
||||||
const val AccessTokenExpired = 1117
|
|
||||||
}
|
|
||||||
|
|
||||||
object VkErrors {
|
|
||||||
const val Unknown = "unknown_error"
|
|
||||||
|
|
||||||
const val NeedValidation = "need_validation"
|
|
||||||
const val NeedCaptcha = "need_captcha"
|
|
||||||
const val InvalidRequest = "invalid_request"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object VkErrorTypes {
|
|
||||||
const val OtpFormatIncorrect = "otp_format_is_incorrect"
|
|
||||||
const val WrongOtp = "wrong_otp"
|
|
||||||
}
|
|
||||||
|
|
||||||
object VkErrorMessages {
|
|
||||||
const val UserBanned = "user has been banned"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class AuthorizationError : ApiError()
|
|
||||||
|
|
||||||
class TokenExpiredError : AuthorizationError()
|
|
||||||
|
|
||||||
data class ValidationRequiredError(
|
|
||||||
@SerializedName("validation_type")
|
|
||||||
val validationType: String,
|
|
||||||
@SerializedName("validation_sid")
|
|
||||||
val validationSid: String,
|
|
||||||
@SerializedName("phone_mask")
|
|
||||||
val phoneMask: String,
|
|
||||||
@SerializedName("redirect_uri")
|
|
||||||
val redirectUri: String,
|
|
||||||
@SerializedName("validation_resend")
|
|
||||||
val validationResend: String
|
|
||||||
) : ApiError()
|
|
||||||
|
|
||||||
data class CaptchaRequiredError(
|
|
||||||
@SerializedName("captcha_sid")
|
|
||||||
val captchaSid: String,
|
|
||||||
@SerializedName("captcha_img")
|
|
||||||
val captchaImg: String
|
|
||||||
) : ApiError()
|
|
||||||
|
|
||||||
object WrongTwoFaCodeFormatError : ApiError()
|
|
||||||
|
|
||||||
object WrongTwoFaCodeError : ApiError()
|
|
||||||
|
|
||||||
data class UserBannedError(
|
|
||||||
@SerializedName("ban_info")
|
|
||||||
val banInfo: BanInfo
|
|
||||||
) : ApiError() {
|
|
||||||
|
|
||||||
data class BanInfo(
|
|
||||||
@SerializedName("member_name")
|
|
||||||
val memberName: String,
|
|
||||||
val message: String,
|
|
||||||
@SerializedName("access_token")
|
|
||||||
val accessToken: String,
|
|
||||||
@SerializedName("restore_url")
|
|
||||||
val restoreUrl: String
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.meloda.fast.api.network
|
|
||||||
|
|
||||||
import com.meloda.fast.api.UserConfig
|
|
||||||
import com.meloda.fast.api.VKConstants
|
|
||||||
import com.meloda.fast.api.network.account.AccountUrls
|
|
||||||
import com.meloda.fast.api.network.ota.OtaUrls
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Response
|
|
||||||
import java.net.URLEncoder
|
|
||||||
|
|
||||||
class AuthInterceptor : Interceptor {
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val builder = chain.request().url.newBuilder()
|
|
||||||
|
|
||||||
val url = builder.build().toUrl().toString()
|
|
||||||
|
|
||||||
if (!url.contains("upload.php") && !url.contains(OtaUrls.GetActualUrl)) {
|
|
||||||
builder.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!url.contains(AccountUrls.SetOnline) && !url.contains("upload.php")) {
|
|
||||||
UserConfig.accessToken.let {
|
|
||||||
if (it.isNotBlank())
|
|
||||||
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
@file:Suppress("UNCHECKED_CAST")
|
|
||||||
|
|
||||||
package com.meloda.fast.api.network
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.meloda.fast.api.VkUtils
|
|
||||||
import com.meloda.fast.api.base.ApiError
|
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
|
||||||
import okhttp3.Request
|
|
||||||
import okio.Timeout
|
|
||||||
import retrofit2.*
|
|
||||||
import java.lang.reflect.ParameterizedType
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
import kotlin.contracts.ExperimentalContracts
|
|
||||||
import kotlin.contracts.contract
|
|
||||||
|
|
||||||
class ResultCallFactory(private val gson: Gson) : CallAdapter.Factory() {
|
|
||||||
override fun get(
|
|
||||||
returnType: Type,
|
|
||||||
annotations: Array<out Annotation>,
|
|
||||||
retrofit: Retrofit,
|
|
||||||
): CallAdapter<*, *>? {
|
|
||||||
val rawReturnType: Class<*> = getRawType(returnType)
|
|
||||||
if (rawReturnType == Call::class.java) {
|
|
||||||
if (returnType is ParameterizedType) {
|
|
||||||
val callInnerType: Type = getParameterUpperBound(0, returnType)
|
|
||||||
if (getRawType(callInnerType) == ApiAnswer::class.java) {
|
|
||||||
if (callInnerType is ParameterizedType) {
|
|
||||||
val resultInnerType = getParameterUpperBound(0, callInnerType)
|
|
||||||
return ResultCallAdapter<Any?>(resultInnerType, gson)
|
|
||||||
}
|
|
||||||
return ResultCallAdapter<Nothing>(Nothing::class.java, gson)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : Call<Out> {
|
|
||||||
|
|
||||||
override fun execute(): Response<Out> = throw NotImplementedError()
|
|
||||||
|
|
||||||
final override fun enqueue(callback: Callback<Out>) = enqueueImpl(callback)
|
|
||||||
|
|
||||||
final override fun clone(): Call<Out> = cloneImpl()
|
|
||||||
|
|
||||||
override fun cancel() = proxy.cancel()
|
|
||||||
|
|
||||||
override fun request(): Request = proxy.request()
|
|
||||||
|
|
||||||
override fun isExecuted() = proxy.isExecuted
|
|
||||||
|
|
||||||
override fun isCanceled() = proxy.isCanceled
|
|
||||||
|
|
||||||
abstract fun enqueueImpl(callback: Callback<Out>)
|
|
||||||
|
|
||||||
abstract fun cloneImpl(): Call<Out>
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ResultCallAdapter<R>(private val type: Type, private val gson: Gson) : CallAdapter<R, Call<ApiAnswer<R>>> {
|
|
||||||
|
|
||||||
override fun responseType() = type
|
|
||||||
|
|
||||||
override fun adapt(call: Call<R>): Call<ApiAnswer<R>> = ResultCall(call, gson)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ResultCall<T>(proxy: Call<T>, private val gson: Gson) : CallDelegate<T, ApiAnswer<T>>(proxy) {
|
|
||||||
|
|
||||||
override fun enqueueImpl(callback: Callback<ApiAnswer<T>>) {
|
|
||||||
proxy.enqueue(ResultCallback(this, callback, gson))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cloneImpl(): ResultCall<T> {
|
|
||||||
return ResultCall(proxy.clone(), gson)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ResultCallback<T>(
|
|
||||||
private val proxy: ResultCall<T>,
|
|
||||||
private val callback: Callback<ApiAnswer<T>>,
|
|
||||||
private val gson: Gson
|
|
||||||
) : Callback<T> {
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
|
||||||
val result: ApiAnswer<T> =
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
val baseBody = response.body()
|
|
||||||
if (baseBody !is ApiResponse<*>) {
|
|
||||||
ApiAnswer.Success(baseBody as T)
|
|
||||||
} else {
|
|
||||||
val body = baseBody as? ApiResponse<*>
|
|
||||||
if (body?.error != null) {
|
|
||||||
VkUtils.getApiError(gson, gson.toJson(body.error))
|
|
||||||
} else {
|
|
||||||
ApiAnswer.Success(body as T)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val errorBodyString = response.errorBody()?.string()
|
|
||||||
|
|
||||||
VkUtils.getApiError(gson, errorBodyString)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkErrors(call, result)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.onResponse(proxy, Response.success(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<T>, error: Throwable) {
|
|
||||||
callback.onResponse(
|
|
||||||
proxy,
|
|
||||||
Response.success(ApiAnswer.Error(ApiError(throwable = error)))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkErrors(call: Call<T>, result: ApiAnswer<*>): Boolean {
|
|
||||||
if (result.isError()) {
|
|
||||||
result.error.throwable?.run {
|
|
||||||
onFailure(call, this)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun timeout(): Timeout {
|
|
||||||
return proxy.timeout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class ApiAnswer<out R> {
|
|
||||||
|
|
||||||
data class Success<out T>(val data: T) : ApiAnswer<T>()
|
|
||||||
data class Error(val error: ApiError) : ApiAnswer<Nothing>()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
fun isSuccessful(): Boolean {
|
|
||||||
contract {
|
|
||||||
returns(true) implies (this@ApiAnswer is Success)
|
|
||||||
}
|
|
||||||
return this is Success
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
fun isError(): Boolean {
|
|
||||||
contract {
|
|
||||||
returns(true) implies (this@ApiAnswer is Error)
|
|
||||||
}
|
|
||||||
return this is Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||