diff --git a/.github/workflows/android_dev.yml b/.github/workflows/android_dev.yml new file mode 100644 index 00000000..ac2cdedd --- /dev/null +++ b/.github/workflows/android_dev.yml @@ -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 diff --git a/.github/workflows/android_master.yml b/.github/workflows/android_master.yml new file mode 100644 index 00000000..d32fee80 --- /dev/null +++ b/.github/workflows/android_master.yml @@ -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 diff --git a/.gitignore b/.gitignore index de251274..7ccead02 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,10 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store -/build +build/ /captures .externalNativeBuild .cxx local.properties -.idea \ No newline at end of file +.idea +/.kotlin diff --git a/README.md b/README.md index b1bdbdf1..bc048887 100644 --- a/README.md +++ b/README.md @@ -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 -Unofficial messenger for russian social network VKontakte +Unofficial messenger for russian social network VKontakte \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore index 42afabfd..3b1ff2da 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +/keystore/keystore.properties +/full diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 39128ba6..ce45158d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,226 +1,184 @@ -@file:Suppress("UnstableApiUsage") - -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 +import java.util.Properties plugins { - id("com.android.application") - id("kotlin-android") - id("kotlin-kapt") - id("kotlin-parcelize") - id("org.jetbrains.kotlin.android") - id("com.google.devtools.ksp") + alias(libs.plugins.com.android.application) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize) + alias(libs.plugins.com.google.devtools.ksp) + alias(libs.plugins.kotlin.compose.compiler) + alias(libs.plugins.kotlin.serialization) } android { - namespace = "com.meloda.fast" - - compileSdk = 34 - - applicationVariants.all { - outputs.all { - (this as BaseVariantOutputImpl).outputFileName = - "${name}-${versionName}-${versionCode}.apk" - } - } + namespace = "com.meloda.app.fast" + compileSdk = Configs.compileSdk defaultConfig { - applicationId = "com.meloda.fast" - minSdk = 24 - targetSdk = 34 - versionCode = 1 - versionName = "alpha" + applicationId = "com.meloda.app.fast" + minSdk = Configs.minSdk + targetSdk = Configs.targetSdk + versionCode = Configs.appCode + versionName = Configs.appName - javaCompileOptions { - annotationProcessorOptions { -// arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + // 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 { - getByName("debug") { - buildConfigField("String", "sdkPackage", sdkPackage) - buildConfigField("String", "sdkFingerprint", sdkFingerprint) - - buildConfigField("String", "msAppCenterAppToken", msAppCenterToken) - - buildConfigField("String", "otaSecretCode", otaSecretCode) - - versionNameSuffix = "_${getVersionName()}" + named("debug") { + signingConfig = signingConfigs.getByName("debugSigning") + applicationIdSuffix = ".debug" } - getByName("release") { - isMinifyEnabled = false + named("release") { + signingConfig = signingConfigs.getByName("release") - buildConfigField("String", "sdkPackage", sdkPackage) - buildConfigField("String", "sdkFingerprint", sdkFingerprint) - - buildConfigField("String", "msAppCenterAppToken", msAppCenterToken) - - buildConfigField("String", "otaSecretCode", otaSecretCode) + isMinifyEnabled = true + isShrinkResources = true 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 productFlavors { - create("dev") { - resourceConfigurations += listOf("en", "xxhdpi") - - dimension = flavorDimension - applicationIdSuffix = ".dev" - versionNameSuffix = "-dev" - } - create("full") { + register("amethyst") { dimension = flavorDimension + isDefault = true } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = Configs.java + targetCompatibility = Configs.java } kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + jvmTarget = Configs.java.toString() freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers") } buildFeatures { - viewBinding = true compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.5" useLiveLiterals = true } - packagingOptions { - jniLibs { - useLegacyPackaging = false - } - } + +// packaging { +// resources { +// excludes += "/META-INF/{AL2.0,LGPL2.1}" +// } +// } } -kapt { - correctErrorTypes = true -} - -fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion" - -val currentTime get() = (System.currentTimeMillis() / 1000).toInt() - 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 - implementation("io.insert-koin:koin-android:3.4.0") - // end of DI zone + // Tests zone + testImplementation(libs.junit) + // 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("io.coil-kt:coil:2.3.0") + implementation(libs.accompanist.permissions) - implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2") - implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2") + // Coil for Compose + 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("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") + implementation(libs.coil) - 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("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 + implementation(libs.androidx.navigation.compose) + implementation(libs.kotlin.serialization) } diff --git a/app/keystore/keystore.jks b/app/keystore/keystore.jks new file mode 100644 index 00000000..1d39b907 Binary files /dev/null and b/app/keystore/keystore.jks differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ff59496d..2f9dc5a4 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/app/schemas/com.meloda.fast.database.AccountsDatabase/2.json b/app/schemas/com.meloda.fast.database.AccountsDatabase/2.json new file mode 100644 index 00000000..116a04f5 --- /dev/null +++ b/app/schemas/com.meloda.fast.database.AccountsDatabase/2.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.meloda.fast.database.CacheDatabase/3.json b/app/schemas/com.meloda.fast.database.CacheDatabase/3.json new file mode 100644 index 00000000..0e8d3d0f --- /dev/null +++ b/app/schemas/com.meloda.fast.database.CacheDatabase/3.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt b/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt new file mode 100644 index 00000000..f436216c --- /dev/null +++ b/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt @@ -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) + } +} diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..5882fe4d Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..ccde9097 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..ff89976e Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..25956942 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..7fc94b56 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/dev/res/values/strings.xml b/app/src/debug/res/values/ic_launcher_background.xml similarity index 52% rename from app/src/dev/res/values/strings.xml rename to app/src/debug/res/values/ic_launcher_background.xml index ede9730a..f2d61e27 100644 --- a/app/src/dev/res/values/strings.xml +++ b/app/src/debug/res/values/ic_launcher_background.xml @@ -1,6 +1,4 @@ - - Fast Dev - + #F44336 diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 00000000..40a4e868 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Fast Debug + diff --git a/app/src/dev/res/mipmap-hdpi/ic_launcher.png b/app/src/dev/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 037e657b..00000000 Binary files a/app/src/dev/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/dev/res/mipmap-mdpi/ic_launcher.png b/app/src/dev/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index e0ee3795..00000000 Binary files a/app/src/dev/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/dev/res/mipmap-xhdpi/ic_launcher.png b/app/src/dev/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index a14bfbf5..00000000 Binary files a/app/src/dev/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png b/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 8b839a55..00000000 Binary files a/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index c3b67b16..00000000 Binary files a/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d0023bd..2cf96e8f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -9,29 +8,22 @@ + android:networkSecurityConfig="@xml/network_security_config" + android:supportsRtl="true" + android:theme="@style/AppTheme"> - - @@ -39,10 +31,8 @@ - - @@ -53,33 +43,6 @@ android:exported="false" android:foregroundServiceType="dataSync" /> - - - - - - - - - - - - - - - - - - - - - = 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 + ) + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt new file mode 100644 index 00000000..8253d8b1 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt @@ -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
{ + 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(startDestination = Conversations) { + friendsRoute( + onError = onError, + navController = navController + ) + conversationsRoute( + onError = onError, + onNavigateToMessagesHistory = onNavigateToMessagesHistory, + navController = navController, + onListScrollingUp = { isScrolling -> +// isBottomBarVisible = isScrolling + } + ) + + composable { + 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) + ) { + + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt b/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt new file mode 100644 index 00000000..5d8b82c3 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt @@ -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 + + val longPollState: StateFlow + val startOnlineService: StateFlow + + 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 + ) + } + } + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt new file mode 100644 index 00000000..806ae91b --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt @@ -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() + 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 + } + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/common/AppGlobal.kt b/app/src/main/kotlin/com/meloda/app/fast/common/AppGlobal.kt new file mode 100644 index 00000000..f76e8f06 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/common/AppGlobal.kt @@ -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() +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt b/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt new file mode 100644 index 00000000..411accb4 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt @@ -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 { androidContext().resources } + factory { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager } + + viewModelOf(::MainViewModelImpl) { + qualifier = qualifier("main") + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt new file mode 100644 index 00000000..6ef44939 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt @@ -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() +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt new file mode 100644 index 00000000..302a100d --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt @@ -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, + 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, + ) + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt new file mode 100644 index 00000000..664bf563 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt @@ -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() +} diff --git a/app/src/main/kotlin/com/meloda/fast/receiver/DownloadManagerReceiver.kt b/app/src/main/kotlin/com/meloda/app/fast/receiver/DownloadManagerReceiver.kt similarity index 89% rename from app/src/main/kotlin/com/meloda/fast/receiver/DownloadManagerReceiver.kt rename to app/src/main/kotlin/com/meloda/app/fast/receiver/DownloadManagerReceiver.kt index 5a80ffda..7b514d11 100644 --- a/app/src/main/kotlin/com/meloda/fast/receiver/DownloadManagerReceiver.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/receiver/DownloadManagerReceiver.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.receiver +package com.meloda.app.fast.receiver import android.content.BroadcastReceiver import android.content.Context @@ -12,4 +12,4 @@ class DownloadManagerReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { onReceiveAction?.invoke() } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/service/OnlineService.kt b/app/src/main/kotlin/com/meloda/app/fast/service/OnlineService.kt new file mode 100644 index 00000000..120e03ff --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/service/OnlineService.kt @@ -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" + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt new file mode 100644 index 00000000..3cb12fb2 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt @@ -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() diff --git a/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/di/LongPollModule.kt b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/di/LongPollModule.kt new file mode 100644 index 00000000..68a42c2f --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/di/LongPollModule.kt @@ -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) +} diff --git a/app/src/main/kotlin/com/meloda/fast/util/NotificationsUtils.kt b/app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt similarity index 93% rename from app/src/main/kotlin/com/meloda/fast/util/NotificationsUtils.kt rename to app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt index efb8294f..523bc569 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/NotificationsUtils.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt @@ -1,11 +1,11 @@ -package com.meloda.fast.util +package com.meloda.app.fast.util import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import com.meloda.fast.R +import com.meloda.app.fast.designsystem.R as UiR object NotificationsUtils { @@ -27,7 +27,7 @@ object NotificationsUtils { actions: List = emptyList(), ): NotificationCompat.Builder { val builder = NotificationCompat.Builder(context, channelId) - .setSmallIcon(R.drawable.ic_fast_logo) + .setSmallIcon(UiR.drawable.ic_fast_logo) .setContentTitle(title) .setPriority(priority.value) .setContentIntent(contentIntent) @@ -69,5 +69,4 @@ object NotificationsUtils { enum class NotificationPriority(val value: Int) { Default(0), Low(-1), Min(-2), High(1), Max(2) } - } diff --git a/app/src/main/kotlin/com/meloda/fast/api/ApiEvent.kt b/app/src/main/kotlin/com/meloda/fast/api/ApiEvent.kt deleted file mode 100644 index bbdad63b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/ApiEvent.kt +++ /dev/null @@ -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 } - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt b/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt deleted file mode 100644 index 83a8ee8c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.api - -object ApiExtensions { - - val Boolean.intString get() = (if (this) 1 else 0).toString() - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt deleted file mode 100644 index d55a408c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ /dev/null @@ -1,1285 +0,0 @@ -package com.meloda.fast.api - -import android.content.Context -import android.graphics.Typeface -import android.graphics.drawable.Drawable -import android.text.SpannableString -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.TextPaint -import android.text.style.ClickableSpan -import android.text.style.StyleSpan -import android.view.View -import androidx.core.content.ContextCompat -import com.google.gson.Gson -import com.meloda.fast.R -import com.meloda.fast.api.base.ApiError -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.attachments.* -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.network.* -import com.meloda.fast.ext.orDots -import com.meloda.fast.model.base.UiImage -import com.meloda.fast.model.base.UiText - -@Suppress("MemberVisibilityCanBePrivate") -object VkUtils { - - fun attachmentToString( - attachmentClass: Class, - id: Int, - ownerId: Int, - withAccessKey: Boolean, - accessKey: String?, - ): String { - val type = when (attachmentClass) { - VkAudio::class.java -> "audio" - VkFile::class.java -> "doc" - VkVideo::class.java -> "video" - VkPhoto::class.java -> "photo" - VkWall::class.java -> "wall" - else -> throw IllegalArgumentException("unknown attachment class: $attachmentClass") - } - - val result = StringBuilder(type).append(ownerId).append('_').append(id) - if (withAccessKey && !accessKey.isNullOrBlank()) { - result.append('_') - result.append(accessKey) - } - return result.toString() - } - - - fun getMessageUser(message: VkMessage, profiles: Map): VkUser? { - return (if (!message.isUser()) null - else profiles[message.fromId]).also { message.user = it } - } - - fun getMessageActionUser(message: VkMessage, profiles: Map): VkUser? { - return if (message.actionMemberId == null || message.actionMemberId <= 0) null - else profiles[message.actionMemberId] - } - - fun getMessageGroup(message: VkMessage, groups: Map): VkGroup? { - return (if (!message.isGroup()) null - else groups[message.fromId]).also { message.group = it } - } - - fun getMessageActionGroup(message: VkMessage, groups: Map): VkGroup? { - return if (message.actionMemberId == null || message.actionMemberId >= 0) null - else groups[message.actionMemberId] - } - - fun getMessageAvatar( - message: VkMessage, - messageUser: VkUser?, - messageGroup: VkGroup?, - ): String? { - return when { - message.isUser() -> messageUser?.photo200 - message.isGroup() -> messageGroup?.photo200 - else -> null - } - } - - fun getMessageTitle( - message: VkMessage, - defMessageUser: VkUser? = null, - defMessageGroup: VkGroup? = null, - profiles: Map? = null, - groups: Map? = null, - ): String? { - val messageUser: VkUser? = - defMessageUser ?: if (profiles == null) null - else profiles[message.fromId] - - val messageGroup: VkGroup? = - defMessageGroup ?: if (groups == null) null - else groups[message.fromId] - - return when { - message.isUser() -> messageUser?.fullName - message.isGroup() -> messageGroup?.name - else -> null - } - } - - fun getConversationUser( - conversation: VkConversationDomain, - profiles: Map - ): VkUser? { - return if (!conversation.isUser()) null - else profiles[conversation.id] - } - - fun getConversationGroup( - conversation: VkConversationDomain, - groups: Map - ): VkGroup? { - return if (!conversation.isGroup()) null - else groups[conversation.id] - } - - fun getConversationAvatar( - conversation: VkConversationDomain, - conversationUser: VkUser?, - conversationGroup: VkGroup?, - ): String? { - return when { - conversation.isAccount() -> null - conversation.isUser() -> conversationUser?.photo200 - conversation.isGroup() -> conversationGroup?.photo200 - conversation.isChat() -> conversation.conversationPhoto - else -> null - } - } - - fun getConversationTitle( - context: Context, - conversation: VkConversationDomain, - defConversationUser: VkUser? = null, - defConversationGroup: VkGroup? = null, - profiles: Map? = null, - groups: Map? = null, - ): String? { - val conversationUser: VkUser? = - defConversationUser ?: if (profiles == null) null - else getConversationUser(conversation, profiles) - - val conversationGroup: VkGroup? = - defConversationGroup ?: if (groups == null) null - else getConversationGroup(conversation, groups) - - return when { - conversation.isAccount() -> context.getString(R.string.favorites) - conversation.isChat() -> conversation.conversationTitle - conversation.isUser() -> conversationUser?.fullName - conversation.isGroup() -> conversationGroup?.name - else -> null - } - } - - fun getConversationUserGroup( - conversation: VkConversationDomain, - profiles: Map, - groups: Map, - ): Pair { - val user: VkUser? = getConversationUser(conversation, profiles) - val group: VkGroup? = getConversationGroup(conversation, groups) - - return user to group - } - - fun getMessageUserGroup( - message: VkMessage?, - profiles: Map, - groups: Map, - ): Pair { - if (message == null) return null to null - - val user: VkUser? = getMessageUser(message, profiles) - val group: VkGroup? = getMessageGroup(message, groups) - - return user to group - } - - fun getMessageActionUserGroup( - message: VkMessage?, - profiles: Map, - groups: Map, - ): Pair { - if (message == null) return null to null - - val user: VkUser? = getMessageActionUser(message, profiles) - val group: VkGroup? = getMessageActionGroup(message, groups) - - return user to group - } - - fun prepareMessageText(text: String, forConversations: Boolean = false): String { - return text.apply { - if (forConversations) { - replace("\n", "") - } - - replace("&", "&") - replace(""", "\"") - replace("
", "\n") - replace(">", ">") - replace("<", "<") - replace("
", "\n") - replace("–", "-") - trim() - } - } - - fun isPreviousMessageSentFiveMinutesAgo(prevMessage: VkMessage?, message: VkMessage?) = - prevMessage != null && message != null && (message.date - prevMessage.date >= 300) - - fun isPreviousMessageFromDifferentSender(prevMessage: VkMessage?, message: VkMessage?) = - prevMessage != null && message != null && prevMessage.fromId != message.fromId - - fun parseForwards(baseForwards: List?): List? { - if (baseForwards.isNullOrEmpty()) return null - - val forwards = mutableListOf() - - for (baseForward in baseForwards) { - forwards += baseForward.asVkMessage() - } - - return forwards - } - - fun parseReplyMessage(baseReplyMessage: BaseVkMessage?): VkMessage? { - if (baseReplyMessage == null) return null - - return baseReplyMessage.asVkMessage() - } - - fun parseAttachments(baseAttachments: List?): List? { - if (baseAttachments.isNullOrEmpty()) return null - - val attachments = mutableListOf() - - for (baseAttachment in baseAttachments) { - when (baseAttachment.getPreparedType()) { - BaseVkAttachmentItem.AttachmentType.Photo -> { - val photo = baseAttachment.photo ?: continue - attachments += photo.asVkPhoto() - } - - BaseVkAttachmentItem.AttachmentType.Video -> { - val video = baseAttachment.video ?: continue - attachments += video.asVkVideo() - } - - BaseVkAttachmentItem.AttachmentType.Audio -> { - val audio = baseAttachment.audio ?: continue - attachments += audio.asVkAudio() - } - - BaseVkAttachmentItem.AttachmentType.File -> { - val file = baseAttachment.file ?: continue - attachments += file.asVkFile() - } - - BaseVkAttachmentItem.AttachmentType.Link -> { - val link = baseAttachment.link ?: continue - attachments += link.asVkLink() - } - - BaseVkAttachmentItem.AttachmentType.MiniApp -> { - val miniApp = baseAttachment.miniApp ?: continue - attachments += miniApp.asVkMiniApp() - } - - BaseVkAttachmentItem.AttachmentType.Voice -> { - val voiceMessage = baseAttachment.voiceMessage ?: continue - attachments += voiceMessage.asVkVoiceMessage() - } - - BaseVkAttachmentItem.AttachmentType.Sticker -> { - val sticker = baseAttachment.sticker ?: continue - attachments += sticker.asVkSticker() - } - - BaseVkAttachmentItem.AttachmentType.Gift -> { - val gift = baseAttachment.gift ?: continue - attachments += gift.asVkGift() - } - - BaseVkAttachmentItem.AttachmentType.Wall -> { - val wall = baseAttachment.wall ?: continue - attachments += wall.asVkWall() - } - - BaseVkAttachmentItem.AttachmentType.Graffiti -> { - val graffiti = baseAttachment.graffiti ?: continue - attachments += graffiti.asVkGraffiti() - } - - BaseVkAttachmentItem.AttachmentType.Poll -> { - val poll = baseAttachment.poll ?: continue - attachments += poll.asVkPoll() - } - - BaseVkAttachmentItem.AttachmentType.WallReply -> { - val wallReply = baseAttachment.wallReply ?: continue - attachments += wallReply.asVkWallReply() - } - - BaseVkAttachmentItem.AttachmentType.Call -> { - val call = baseAttachment.call ?: continue - attachments += call.asVkCall() - } - - BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> { - val groupCall = baseAttachment.groupCall ?: continue - attachments += groupCall.asVkGroupCall() - } - - BaseVkAttachmentItem.AttachmentType.Curator -> { - val curator = baseAttachment.curator ?: continue - attachments += curator.asVkCurator() - } - - BaseVkAttachmentItem.AttachmentType.Event -> { - val event = baseAttachment.event ?: continue - attachments += event.asVkEvent() - } - - BaseVkAttachmentItem.AttachmentType.Story -> { - val story = baseAttachment.story ?: continue - attachments += story.asVkStory() - } - - BaseVkAttachmentItem.AttachmentType.Widget -> { - val widget = baseAttachment.widget ?: continue - attachments += widget.asVkWidget() - } - - else -> continue - } - } - - return attachments - } - - fun getActionMessageText( - message: VkMessage?, - youPrefix: String, - messageUser: VkUser?, - messageGroup: VkGroup?, - action: VkMessage.Action?, - actionUser: VkUser?, - actionGroup: VkGroup?, - ): UiText? { - if (message == null) return null - - return when (action) { - VkMessage.Action.CHAT_CREATE -> { - val text = message.actionText ?: return null - - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_created, - listOf(prefix, text) - ) - } - - VkMessage.Action.CHAT_TITLE_UPDATE -> { - val text = message.actionText ?: return null - - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_renamed, - listOf(prefix, text) - ) - } - - VkMessage.Action.CHAT_PHOTO_UPDATE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_photo_update, listOf(prefix)) - } - - VkMessage.Action.CHAT_PHOTO_REMOVE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_photo_remove, listOf(prefix)) - } - - VkMessage.Action.CHAT_KICK_USER -> { - val memberId = message.actionMemberId ?: return null - val isUser = memberId > 0 - val isGroup = memberId < 0 - - if (isUser && actionUser == null) return null - if (isGroup && actionGroup == null) return null - - if (memberId == message.fromId) { - val prefix = if (memberId == UserConfig.userId) youPrefix - else actionUser.toString() - - UiText.ResourceParams(R.string.message_action_chat_user_left, listOf(prefix)) - } else { - val prefix = - if (message.fromId == UserConfig.userId) youPrefix - else messageUser?.toString() ?: messageGroup?.toString().orDots() - - val postfix = - if (memberId == UserConfig.userId) youPrefix.lowercase() - else actionUser.toString() - - UiText.ResourceParams( - R.string.message_action_chat_user_kicked, listOf(prefix, postfix) - ) - } - } - - VkMessage.Action.CHAT_INVITE_USER -> { - val memberId = message.actionMemberId ?: 0 - val isUser = memberId > 0 - val isGroup = memberId < 0 - - if (isUser && actionUser == null) return null - if (isGroup && actionGroup == null) return null - - if (memberId == message.fromId) { - val prefix = if (memberId == UserConfig.userId) youPrefix - else actionUser.toString() - - UiText.ResourceParams( - R.string.message_action_chat_user_returned, - listOf(prefix) - ) - } else { - val prefix = if (message.fromId == UserConfig.userId) youPrefix - else messageUser?.toString() ?: messageGroup?.toString().orDots() - - val postfix = - if (memberId == UserConfig.userId) youPrefix.lowercase() - else actionUser.toString() - - UiText.ResourceParams( - R.string.message_action_chat_user_invited, - listOf(prefix, postfix) - ) - } - } - - VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_user_joined_by_link, - listOf(prefix) - ) - } - - VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_user_joined_by_call, - listOf(prefix) - ) - } - - VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_user_joined_by_call_link, - listOf(prefix) - ) - } - - VkMessage.Action.CHAT_PIN_MESSAGE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_pin_message, listOf(prefix)) - } - - VkMessage.Action.CHAT_UNPIN_MESSAGE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_unpin_message, listOf(prefix)) - } - - VkMessage.Action.CHAT_SCREENSHOT -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_screenshot, listOf(prefix)) - } - - VkMessage.Action.CHAT_STYLE_UPDATE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_style_update, listOf(prefix)) - } - - null -> null - } - } - - fun getActionMessageText( - context: Context, - message: VkMessage?, - youPrefix: String, - messageUser: VkUser?, - messageGroup: VkGroup?, - action: VkMessage.Action?, - actionUser: VkUser?, - actionGroup: VkGroup?, - ): SpannableString? { - if (message == null) return null - - return when (action) { - VkMessage.Action.CHAT_CREATE -> { - val text = message.actionText ?: return null - - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_created, prefix, text) - - val startIndex = spanText.indexOf(text, startIndex = prefix.length) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - it.setSpan( - StyleSpan(Typeface.BOLD), - startIndex, - startIndex + text.length, 0 - ) - } - } - - VkMessage.Action.CHAT_TITLE_UPDATE -> { - val text = message.actionText ?: return null - - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_renamed, prefix, text) - val startIndex = spanText.indexOf(text) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - it.setSpan( - StyleSpan(Typeface.BOLD), startIndex, startIndex + text.length, 0 - ) - } - } - - VkMessage.Action.CHAT_PHOTO_UPDATE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_photo_update, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_PHOTO_REMOVE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_photo_remove, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_KICK_USER -> { - val memberId = message.actionMemberId ?: return null - val isUser = memberId > 0 - val isGroup = memberId < 0 - - if (isUser && actionUser == null) return null - if (isGroup && actionGroup == null) return null - - if (memberId == message.fromId) { - val prefix = if (memberId == UserConfig.userId) youPrefix - else actionUser.toString() - - val spanText = - context.getString(R.string.message_action_chat_user_left, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } else { - val prefix = - if (message.fromId == UserConfig.userId) youPrefix - else messageUser?.toString() ?: messageGroup?.toString().orDots() - - val postfix = - if (memberId == UserConfig.userId) youPrefix.lowercase() - else actionUser.toString() - - val spanText = - context.getString( - R.string.message_action_chat_user_kicked, - prefix, - postfix - ) - val startIndex = spanText.indexOf(postfix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - it.setSpan( - StyleSpan(Typeface.BOLD), startIndex, startIndex + postfix.length, 0 - ) - } - } - } - - VkMessage.Action.CHAT_INVITE_USER -> { - val memberId = message.actionMemberId ?: 0 - val isUser = memberId > 0 - val isGroup = memberId < 0 - - if (isUser && actionUser == null) return null - if (isGroup && actionGroup == null) return null - - if (memberId == message.fromId) { - val prefix = if (memberId == UserConfig.userId) youPrefix - else actionUser.toString() - - val spanText = - context.getString(R.string.message_action_chat_user_returned, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } else { - val prefix = if (message.fromId == UserConfig.userId) youPrefix - else messageUser?.toString() ?: messageGroup?.toString().orDots() - - val postfix = - if (memberId == UserConfig.userId) youPrefix.lowercase() - else actionUser.toString() - - val spanText = - context.getString( - R.string.message_action_chat_user_invited, - prefix, - postfix - ) - val startIndex = spanText.indexOf(postfix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - it.setSpan( - StyleSpan(Typeface.BOLD), startIndex, startIndex + postfix.length, 0 - ) - } - } - } - - VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_user_joined_by_link, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_user_joined_by_call, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_user_joined_by_call_link, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_PIN_MESSAGE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_pin_message, prefix).trim() - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_UNPIN_MESSAGE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_unpin_message, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_SCREENSHOT -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_screenshot, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_STYLE_UPDATE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_style_update, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - null -> null - } - } - - fun getActionConversationText( - message: VkMessage?, - youPrefix: String, - messageUser: VkUser? = null, - messageGroup: VkGroup? = null, - action: VkMessage.Action?, - actionUser: VkUser?, - actionGroup: VkGroup?, - ): UiText? { - return getActionMessageText( - message = message, - youPrefix = youPrefix, - messageUser = messageUser, - messageGroup = messageGroup, - action = action, - actionUser = actionUser, - actionGroup = actionGroup, - ) - } - - fun getActionConversationText( - context: Context, - message: VkMessage?, - youPrefix: String, - messageUser: VkUser? = null, - messageGroup: VkGroup? = null, - action: VkMessage.Action?, - actionUser: VkUser?, - actionGroup: VkGroup?, - ): String? { - return getActionMessageText( - context = context, - message = message, - youPrefix = youPrefix, - messageUser = messageUser, - messageGroup = messageGroup, - action = action, - actionUser = actionUser, - actionGroup = actionGroup, - )?.toString() - } - - fun getForwardsText(message: VkMessage?): UiText? { - if (message?.forwards.isNullOrEmpty()) return null - - return message?.forwards?.let { forwards -> - UiText.Resource( - if (forwards.size == 1) R.string.forwarded_message - else R.string.forwarded_messages - ) - } - } - - fun getAttachmentText(message: VkMessage?): UiText? { - message?.geo?.let { - return when (it.type) { - "point" -> UiText.Resource(R.string.message_geo_point) - else -> UiText.Resource(R.string.message_geo) - } - } - if (message?.attachments.isNullOrEmpty()) return null - - return message?.attachments?.let { attachments -> - if (attachments.size == 1) { - getAttachmentTypeByClass(attachments[0])?.let { - getAttachmentTextByType(it) - } - } else { - if (isAttachmentsHaveOneType(attachments)) { - getAttachmentTypeByClass(attachments[0])?.let { - getAttachmentTextByType(it, attachments.size) - } - } else { - UiText.Resource(R.string.message_attachments_many) - } - } - } - } - - fun getAttachmentConversationIcon(message: VkMessage?): UiImage? { - return message?.attachments?.let { attachments -> - if (attachments.isEmpty()) return null - if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) { - message.geo?.let { - return UiImage.Resource(R.drawable.ic_map_marker) - } - - getAttachmentTypeByClass(attachments[0])?.let { - getAttachmentIconByType(it) - } - } else { - UiImage.Resource(R.drawable.ic_baseline_attach_file_24) - } - } - } - - fun getAttachmentConversationIcon( - context: Context, - message: VkMessage?, - ): Drawable? { - if (message == null) return null - return message.attachments?.let { attachments -> - if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) { - message.geo?.let { - return ContextCompat.getDrawable(context, R.drawable.ic_map_marker) - } - - if (attachments.isEmpty()) return null - - getAttachmentTypeByClass(attachments[0])?.let { - getAttachmentIconByType( - context, - it - ) - } - } else { - ContextCompat.getDrawable(context, R.drawable.ic_baseline_attach_file_24) - } - } - } - - fun getAttachmentIconByType(attachmentType: BaseVkAttachmentItem.AttachmentType): UiImage? { - return when (attachmentType) { - BaseVkAttachmentItem.AttachmentType.Photo -> R.drawable.ic_attachment_photo - BaseVkAttachmentItem.AttachmentType.Video -> R.drawable.ic_attachment_video - BaseVkAttachmentItem.AttachmentType.Audio -> R.drawable.ic_attachment_audio - BaseVkAttachmentItem.AttachmentType.File -> R.drawable.ic_attachment_file - BaseVkAttachmentItem.AttachmentType.Link -> R.drawable.ic_attachment_link - BaseVkAttachmentItem.AttachmentType.Voice -> R.drawable.ic_attachment_voice - BaseVkAttachmentItem.AttachmentType.MiniApp -> R.drawable.ic_attachment_mini_app - BaseVkAttachmentItem.AttachmentType.Sticker -> R.drawable.ic_attachment_sticker - BaseVkAttachmentItem.AttachmentType.Gift -> R.drawable.ic_attachment_gift - BaseVkAttachmentItem.AttachmentType.Wall -> R.drawable.ic_attachment_wall - BaseVkAttachmentItem.AttachmentType.Graffiti -> R.drawable.ic_attachment_graffiti - BaseVkAttachmentItem.AttachmentType.Poll -> R.drawable.ic_attachment_poll - BaseVkAttachmentItem.AttachmentType.WallReply -> R.drawable.ic_attachment_wall_reply - BaseVkAttachmentItem.AttachmentType.Call -> R.drawable.ic_attachment_call - BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> R.drawable.ic_attachment_group_call - BaseVkAttachmentItem.AttachmentType.Story -> R.drawable.ic_attachment_story - else -> null - }?.let(UiImage::Resource) - } - - @Deprecated("Use new with UiImage") - fun getAttachmentIconByType( - context: Context, - attachmentType: BaseVkAttachmentItem.AttachmentType, - ): Drawable? { - val resId = when (attachmentType) { - BaseVkAttachmentItem.AttachmentType.Photo -> R.drawable.ic_attachment_photo - BaseVkAttachmentItem.AttachmentType.Video -> R.drawable.ic_attachment_video - BaseVkAttachmentItem.AttachmentType.Audio -> R.drawable.ic_attachment_audio - BaseVkAttachmentItem.AttachmentType.File -> R.drawable.ic_attachment_file - BaseVkAttachmentItem.AttachmentType.Link -> R.drawable.ic_attachment_link - BaseVkAttachmentItem.AttachmentType.Voice -> R.drawable.ic_attachment_voice - BaseVkAttachmentItem.AttachmentType.MiniApp -> R.drawable.ic_attachment_mini_app - BaseVkAttachmentItem.AttachmentType.Sticker -> R.drawable.ic_attachment_sticker - BaseVkAttachmentItem.AttachmentType.Gift -> R.drawable.ic_attachment_gift - BaseVkAttachmentItem.AttachmentType.Wall -> R.drawable.ic_attachment_wall - BaseVkAttachmentItem.AttachmentType.Graffiti -> R.drawable.ic_attachment_graffiti - BaseVkAttachmentItem.AttachmentType.Poll -> R.drawable.ic_attachment_poll - BaseVkAttachmentItem.AttachmentType.WallReply -> R.drawable.ic_attachment_wall_reply - BaseVkAttachmentItem.AttachmentType.Call -> R.drawable.ic_attachment_call - BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> R.drawable.ic_attachment_group_call - BaseVkAttachmentItem.AttachmentType.Story -> R.drawable.ic_attachment_story - else -> return null - } - - return ContextCompat.getDrawable(context, resId) - } - - fun isAttachmentsHaveOneType(attachments: List): Boolean { - if (attachments.isEmpty()) return true - if (attachments.size == 1) return true - - val firstType = getAttachmentTypeByClass(attachments[0]) - for (i in 1 until attachments.size) { - val type = getAttachmentTypeByClass(attachments[i]) - if (type != firstType) return false - } - - return true - } - - fun getAttachmentTypeByClass(attachment: VkAttachment): BaseVkAttachmentItem.AttachmentType? { - return when (attachment) { - is VkPhoto -> BaseVkAttachmentItem.AttachmentType.Photo - is VkVideo -> BaseVkAttachmentItem.AttachmentType.Video - is VkAudio -> BaseVkAttachmentItem.AttachmentType.Audio - is VkFile -> BaseVkAttachmentItem.AttachmentType.File - is VkLink -> BaseVkAttachmentItem.AttachmentType.Link - is VkMiniApp -> BaseVkAttachmentItem.AttachmentType.MiniApp - is VkVoiceMessage -> BaseVkAttachmentItem.AttachmentType.Voice - is VkSticker -> BaseVkAttachmentItem.AttachmentType.Sticker - is VkGift -> BaseVkAttachmentItem.AttachmentType.Gift - is VkWall -> BaseVkAttachmentItem.AttachmentType.Wall - is VkGraffiti -> BaseVkAttachmentItem.AttachmentType.Graffiti - is VkPoll -> BaseVkAttachmentItem.AttachmentType.Poll - is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WallReply - is VkCall -> BaseVkAttachmentItem.AttachmentType.Call - is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GroupCallInProgress - is VkEvent -> BaseVkAttachmentItem.AttachmentType.Event - is VkCurator -> BaseVkAttachmentItem.AttachmentType.Curator - is VkStory -> BaseVkAttachmentItem.AttachmentType.Story - is VkWidget -> BaseVkAttachmentItem.AttachmentType.Widget - else -> null - } - } - - fun getAttachmentTextByType( - attachmentType: BaseVkAttachmentItem.AttachmentType, - size: Int = 1, - ): UiText { - return when (attachmentType) { - BaseVkAttachmentItem.AttachmentType.Photo -> - UiText.QuantityResource(R.plurals.attachment_photos, size) - - BaseVkAttachmentItem.AttachmentType.Video -> - UiText.QuantityResource(R.plurals.attachment_videos, size) - - BaseVkAttachmentItem.AttachmentType.Audio -> - UiText.QuantityResource(R.plurals.attachment_audios, size) - - BaseVkAttachmentItem.AttachmentType.File -> - UiText.QuantityResource(R.plurals.attachment_files, size) - - BaseVkAttachmentItem.AttachmentType.Link -> - UiText.Resource(R.string.message_attachments_link) - - BaseVkAttachmentItem.AttachmentType.Voice -> - UiText.Resource(R.string.message_attachments_voice) - - BaseVkAttachmentItem.AttachmentType.MiniApp -> - UiText.Resource(R.string.message_attachments_mini_app) - - BaseVkAttachmentItem.AttachmentType.Sticker -> - UiText.Resource(R.string.message_attachments_sticker) - - BaseVkAttachmentItem.AttachmentType.Gift -> - UiText.Resource(R.string.message_attachments_gift) - - BaseVkAttachmentItem.AttachmentType.Wall -> - UiText.Resource(R.string.message_attachments_wall) - - BaseVkAttachmentItem.AttachmentType.Graffiti -> - UiText.Resource(R.string.message_attachments_graffiti) - - BaseVkAttachmentItem.AttachmentType.Poll -> - UiText.Resource(R.string.message_attachments_poll) - - BaseVkAttachmentItem.AttachmentType.WallReply -> - UiText.Resource(R.string.message_attachments_wall_reply) - - BaseVkAttachmentItem.AttachmentType.Call -> - UiText.Resource(R.string.message_attachments_call) - - BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> - UiText.Resource(R.string.message_attachments_call_in_progress) - - BaseVkAttachmentItem.AttachmentType.Event -> - UiText.Resource(R.string.message_attachments_event) - - BaseVkAttachmentItem.AttachmentType.Curator -> - UiText.Resource(R.string.message_attachments_curator) - - BaseVkAttachmentItem.AttachmentType.Story -> - UiText.Resource(R.string.message_attachments_story) - - BaseVkAttachmentItem.AttachmentType.Widget -> - UiText.Resource(R.string.message_attachments_widget) - - else -> UiText.Simple(attachmentType.value) - } - } - - fun getApiError(gson: Gson, errorString: String?): ApiAnswer.Error { - try { - val defaultError = gson.fromJson(errorString, ApiError::class.java) - - val error: ApiError = - when (defaultError.error) { - VkErrorCodes.UserAuthorizationFailed.toString() -> { - val authorizationError = - gson.fromJson(errorString, AuthorizationError::class.java) - - authorizationError - } - - VkErrorCodes.AccessTokenExpired.toString() -> { - val tokenExpiredError = - gson.fromJson(errorString, TokenExpiredError::class.java) - - tokenExpiredError - } - - VkErrors.NeedValidation -> { - val validationError = - gson.fromJson( - errorString, - if (defaultError.errorMessage == VkErrorMessages.UserBanned) { - UserBannedError::class.java - } else { - ValidationRequiredError::class.java - } - ) - - validationError - } - - VkErrors.NeedCaptcha -> { - val captchaRequiredError = - gson.fromJson(errorString, CaptchaRequiredError::class.java) - - captchaRequiredError - } - - VkErrors.InvalidRequest -> { - when (defaultError.errorType) { - VkErrorTypes.OtpFormatIncorrect -> WrongTwoFaCodeFormatError - VkErrorTypes.WrongOtp -> WrongTwoFaCodeError - else -> defaultError - } - } - - else -> defaultError - } - - return ApiAnswer.Error(error) - } catch (e: Exception) { - return ApiAnswer.Error(ApiError(throwable = e)) - } - } - - fun visualizeMentions( - messageText: String, - mentionColor: Int, - onMentionClick: ((id: Int) -> Unit)? = null, - ): SpannableStringBuilder { - if (messageText.isEmpty()) { - return SpannableStringBuilder("") - } - - var newMessageText = messageText - - val idsIndexes = mutableListOf>() - val mentions = mutableListOf>() - - var startFrom = 0 - - while (true) { - val leftBracketIndex = newMessageText.indexOf('[', startFrom) - val verticalLineIndex = newMessageText.indexOf('|', startFrom) - val rightBracketIndex = newMessageText.indexOf(']', startFrom) - - if (leftBracketIndex == -1 || - verticalLineIndex == -1 || - rightBracketIndex == -1 - ) break - - val idPart = newMessageText.substring(leftBracketIndex + 1, verticalLineIndex) - - val actualId = idPart.substring(2, idPart.length).toIntOrNull() ?: -1 - - if (!idPart.matches(Regex("^id(\\d+)\$")) || rightBracketIndex - verticalLineIndex < 2) { - break - } - - val text = newMessageText.substring(verticalLineIndex + 1, rightBracketIndex) - - val str = "[$idPart|$text]" - - mentions += str to text - - idsIndexes += Triple(actualId, leftBracketIndex, leftBracketIndex + text.length) - - startFrom = rightBracketIndex + 1 - } - - idsIndexes.reverse() - - mentions.forEachIndexed { index, pair -> - val old = pair.first - val new = pair.second - - val oldIndexStart = newMessageText.indexOf(old) - - idsIndexes[index].copy( - second = oldIndexStart, - third = oldIndexStart + new.length - ).let { idsIndexes[index] = it } - - newMessageText = newMessageText.replace(old, new) - } - - val spanBuilder = SpannableStringBuilder(newMessageText) - - idsIndexes.forEach { triple -> - val id = triple.first - val start = triple.second - val end = triple.third - - spanBuilder.setSpan( - createClickableSpan(id, mentionColor, onMentionClick), - start, - end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - - return spanBuilder - } - - private fun createClickableSpan( - id: Int, - mentionColor: Int, - onMentionClick: ((id: Int) -> Unit)? = null, - ): ClickableSpan { - return object : ClickableSpan() { - override fun onClick(widget: View) { - widget.cancelPendingInputEvents() - - onMentionClick?.invoke(id) - } - - override fun updateDrawState(ds: TextPaint) { - ds.color = mentionColor -// ds.typeface = Typeface.defaultFromStyle(Typeface.BOLD) - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt b/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt deleted file mode 100644 index ae38560f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/base/ApiResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/base/ApiResponse.kt deleted file mode 100644 index 51c3cd35..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/base/ApiResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.api.base - -data class ApiResponse( - val error: ApiError? = null, - val response: T? = null -) { - val isSuccessful get() = error == null && response != null -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/base/AttachmentClassNameIsEmptyException.kt b/app/src/main/kotlin/com/meloda/fast/api/base/AttachmentClassNameIsEmptyException.kt deleted file mode 100644 index 2aad4520..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/base/AttachmentClassNameIsEmptyException.kt +++ /dev/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\"" - ) diff --git a/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollEvent.kt b/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollEvent.kt deleted file mode 100644 index 3637bd5f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollEvent.kt +++ /dev/null @@ -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, - val groups: HashMap, - ) : 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() - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollUpdatesParser.kt b/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollUpdatesParser.kt deleted file mode 100644 index 324d446f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollUpdatesParser.kt +++ /dev/null @@ -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>> = - 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) - .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) - .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) - .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) - .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) - .onEvent( - LongPollEvent.VkMessageReadOutgoingEvent( - peerId = peerId, - messageId = messageId, - unreadCount = unreadCount - ) - ) - } - } - } - } - - private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - } - - private suspend fun 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() - messagesResponse.profiles?.forEach { baseUser -> - baseUser.mapToDomain().let { user -> profiles[user.id] = user } - } - - val groups = hashMapOf() - 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 registerListener(eventType: ApiEvent, listener: VkEventCallback) { - listenersMap.let { map -> - map[eventType] = (map[eventType] ?: mutableListOf()).also { - it.add(listener) - } - } - } - - fun onConversationPinStateChanged(listener: VkEventCallback) { - registerListener(ApiEvent.PinUnpinConversation, listener) - } - - fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) { - onConversationPinStateChanged(assembleEventCallback(block)) - } - - fun onMessageIncomingRead(listener: VkEventCallback) { - registerListener(ApiEvent.MessageReadIncoming, listener) - } - - fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) { - onMessageIncomingRead(assembleEventCallback(block)) - } - - fun onMessageOutgoingRead(listener: VkEventCallback) { - registerListener(ApiEvent.MessageReadOutgoing, listener) - } - - fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) { - onMessageOutgoingRead(assembleEventCallback(block)) - } - - fun onNewMessage(listener: VkEventCallback) { - registerListener(ApiEvent.MessageNew, listener) - } - - fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) { - onNewMessage(assembleEventCallback(block)) - } - - fun onMessageEdited(listener: VkEventCallback) { - registerListener(ApiEvent.MessageEdit, listener) - } - - fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) { - onMessageEdited(assembleEventCallback(block)) - } - - fun clearListeners() { - listenersMap.clear() - } -} - -internal inline fun assembleEventCallback( - crossinline block: (R) -> Unit, -): VkEventCallback { - return VkEventCallback { event -> block.invoke(event) } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/ActionState.kt b/app/src/main/kotlin/com/meloda/fast/api/model/ActionState.kt deleted file mode 100644 index ece31811..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/ActionState.kt +++ /dev/null @@ -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 - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/ConversationPeerType.kt b/app/src/main/kotlin/com/meloda/fast/api/model/ConversationPeerType.kt deleted file mode 100644 index 25c3bb53..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/ConversationPeerType.kt +++ /dev/null @@ -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 - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkChatMember.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkChatMember.kt deleted file mode 100644 index 1a513309..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkChatMember.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt deleted file mode 100644 index 0e3469a4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt +++ /dev/null @@ -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() - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt deleted file mode 100644 index 55373d72..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt +++ /dev/null @@ -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? = null, - var attachments: List? = 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 - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt deleted file mode 100644 index fdd2f9e2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt +++ /dev/null @@ -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() - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt deleted file mode 100644 index 71702bfb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt +++ /dev/null @@ -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) = "" - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt deleted file mode 100644 index 1427cf2f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt +++ /dev/null @@ -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 - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt deleted file mode 100644 index 7a6c4002..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt +++ /dev/null @@ -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 - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCurator.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCurator.kt deleted file mode 100644 index dd656e4d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCurator.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkEvent.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkEvent.kt deleted file mode 100644 index debda085..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkEvent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkEvent( - val id: Int -) : VkAttachment() diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt deleted file mode 100644 index 2766e61b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt +++ /dev/null @@ -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 - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt deleted file mode 100644 index 6be29fca..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt deleted file mode 100644 index 2f4ddae9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGroupCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGroupCall.kt deleted file mode 100644 index 2a0aa581..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGroupCall.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt deleted file mode 100644 index 321c71ac..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkMiniApp.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkMiniApp.kt deleted file mode 100644 index a65fb1f1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkMiniApp.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPoll.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPoll.kt deleted file mode 100644 index 4bee6a71..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPoll.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt deleted file mode 100644 index da6c4c82..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt +++ /dev/null @@ -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, - val backgroundImages: List -) : 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 - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkStory.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkStory.kt deleted file mode 100644 index c6862706..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkStory.kt +++ /dev/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 - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt deleted file mode 100644 index 557f4c77..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt +++ /dev/null @@ -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, - val linkOgg: String, - val linkMp3: String, - val accessKey: String, - val transcriptState: String?, - val transcript: String? -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWall.kt deleted file mode 100644 index bd24073a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWall.kt +++ /dev/null @@ -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?, - 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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWallReply.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWallReply.kt deleted file mode 100644 index 110145eb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWallReply.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt deleted file mode 100644 index 7edb329f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChat.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChat.kt deleted file mode 100644 index 1f10f4c5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChat.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChatMember.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChatMember.kt deleted file mode 100644 index 139664d9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChatMember.kt +++ /dev/null @@ -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 - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt deleted file mode 100644 index e76b3ea7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt +++ /dev/null @@ -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 - ) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt deleted file mode 100644 index cf5d0814..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt deleted file mode 100644 index 46a5b7fa..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt +++ /dev/null @@ -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? = emptyList(), - val important: Boolean, - val random_id: Int, - val attachments: List = 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 - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt deleted file mode 100644 index 322ce4d2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt +++ /dev/null @@ -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 - ) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAttachmentItem.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAttachmentItem.kt deleted file mode 100644 index 51f0138f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAttachmentItem.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt deleted file mode 100644 index 09de47b9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt deleted file mode 100644 index 2bbde082..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt +++ /dev/null @@ -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 - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCurator.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCurator.kt deleted file mode 100644 index a84b6e85..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCurator.kt +++ /dev/null @@ -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 -) : BaseVkAttachment() { - - fun asVkCurator() = VkCurator( - id = id - ) - - @Parcelize - data class Photo( - val height: Int, - val url: String, - val width: String - ) : Parcelable - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkEvent.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkEvent.kt deleted file mode 100644 index a1b09ce8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkEvent.kt +++ /dev/null @@ -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 = emptyList(), - val member_status: Int, - val time: Int -) : BaseVkAttachment() { - - fun asVkEvent() = VkEvent(id = id) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt deleted file mode 100644 index edc8b96d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt +++ /dev/null @@ -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) : 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 - - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt deleted file mode 100644 index 29e646b9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt +++ /dev/null @@ -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 - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt deleted file mode 100644 index c5e841ef..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt +++ /dev/null @@ -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 - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGroupCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGroupCall.kt deleted file mode 100644 index e9ff17c0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGroupCall.kt +++ /dev/null @@ -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, - val count: Int - ) : Parcelable - - fun asVkGroupCall() = VkGroupCall(initiatorId = initiator_id) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt deleted file mode 100644 index 4f1b59c5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt +++ /dev/null @@ -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 - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkMiniApp.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkMiniApp.kt deleted file mode 100644 index 8e857850..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkMiniApp.kt +++ /dev/null @@ -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?, - 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) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt deleted file mode 100644 index babe40e4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt +++ /dev/null @@ -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, - 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 - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPoll.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPoll.kt deleted file mode 100644 index 521145bb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPoll.kt +++ /dev/null @@ -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?, - val embed_hash: String, - val answers: List, - 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 - ) : Parcelable { - - @Parcelize - data class Point( - val color: String, - val position: Double - ) : Parcelable - } - - fun asVkPoll() = VkPoll(id = id) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt deleted file mode 100644 index 79e053d1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt +++ /dev/null @@ -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, - val images_with_background: List, - val animation_url: String?, - val animations: List? -) : 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 - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkStory.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkStory.kt deleted file mode 100644 index d16f2d51..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkStory.kt +++ /dev/null @@ -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 - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt deleted file mode 100644 index 0cce54de..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt +++ /dev/null @@ -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, - val first_frame: List, - 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, - 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, - val midroll_percents: List - ) : 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 - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt deleted file mode 100644 index 23088e26..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt +++ /dev/null @@ -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, - 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 - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWall.kt deleted file mode 100644 index e1dee465..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWall.kt +++ /dev/null @@ -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?, - 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 - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt deleted file mode 100644 index a6bf1006..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt +++ /dev/null @@ -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) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/data/BaseVkConversation.kt b/app/src/main/kotlin/com/meloda/fast/api/model/data/BaseVkConversation.kt deleted file mode 100644 index 933f778b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/data/BaseVkConversation.kt +++ /dev/null @@ -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, - val active_ids: List, - 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, - 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 - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/domain/VkConversationDomain.kt b/app/src/main/kotlin/com/meloda/fast/api/model/domain/VkConversationDomain.kt deleted file mode 100644 index 89f4d731..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/domain/VkConversationDomain.kt +++ /dev/null @@ -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 - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/presentation/VkConversationUi.kt b/app/src/main/kotlin/com/meloda/fast/api/model/presentation/VkConversationUi.kt deleted file mode 100644 index 2f731153..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/presentation/VkConversationUi.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ApiErrors.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ApiErrors.kt deleted file mode 100644 index 057ebb6c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ApiErrors.kt +++ /dev/null @@ -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 - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt deleted file mode 100644 index 32c4ca2f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt +++ /dev/null @@ -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()) - - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt deleted file mode 100644 index 2cde43e2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt +++ /dev/null @@ -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, - 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(resultInnerType, gson) - } - return ResultCallAdapter(Nothing::class.java, gson) - } - } - } - return null - } -} - -internal abstract class CallDelegate(protected val proxy: Call) : Call { - - override fun execute(): Response = throw NotImplementedError() - - final override fun enqueue(callback: Callback) = enqueueImpl(callback) - - final override fun clone(): Call = 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) - - abstract fun cloneImpl(): Call -} - -private class ResultCallAdapter(private val type: Type, private val gson: Gson) : CallAdapter>> { - - override fun responseType() = type - - override fun adapt(call: Call): Call> = ResultCall(call, gson) -} - -internal class ResultCall(proxy: Call, private val gson: Gson) : CallDelegate>(proxy) { - - override fun enqueueImpl(callback: Callback>) { - proxy.enqueue(ResultCallback(this, callback, gson)) - } - - override fun cloneImpl(): ResultCall { - return ResultCall(proxy.clone(), gson) - } - - private class ResultCallback( - private val proxy: ResultCall, - private val callback: Callback>, - private val gson: Gson - ) : Callback { - - override fun onResponse(call: Call, response: Response) { - val result: ApiAnswer = - 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, error: Throwable) { - callback.onResponse( - proxy, - Response.success(ApiAnswer.Error(ApiError(throwable = error))) - ) - } - - private fun checkErrors(call: Call, 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 { - - data class Success(val data: T) : ApiAnswer() - data class Error(val error: ApiError) : ApiAnswer() - - @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 - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt deleted file mode 100644 index a38e540e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.api.network - -object VkUrls { - - const val OAUTH = "https://oauth.vk.com" - const val API = "https://api.vk.com/method" -} - - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt deleted file mode 100644 index c3588a2c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.meloda.fast.api.network.account - -import com.meloda.fast.api.network.VkUrls - -object AccountUrls { - - const val SetOnline = "${VkUrls.API}/account.setOnline" - const val SetOffline = "${VkUrls.API}/account.setOffline" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosRequests.kt deleted file mode 100644 index dfa5fbf3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosRequests.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.network.audio - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosResponses.kt deleted file mode 100644 index a0a39180..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosResponses.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.meloda.fast.api.network.audio - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AudiosGetUploadServerResponse( - @SerializedName("upload_url") - val uploadUrl: String -) : Parcelable - -@Parcelize -data class AudiosUploadResponse( - val redirect: String, - val server: Int, - val audio: String?, - val hash: String, - val error: String? -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosUrls.kt deleted file mode 100644 index 094f32fa..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosUrls.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.api.network.audio - -import com.meloda.fast.api.network.VkUrls - -object AudiosUrls { - - const val GetUploadServer = "${VkUrls.API}/audio.getUploadServer" - - const val Save = "${VkUrls.API}/audio.save" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt deleted file mode 100644 index f206f415..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.network.auth - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AuthDirectResponse( - @SerializedName("access_token") val accessToken: String?, - @SerializedName("user_id") val userId: Int?, - @SerializedName("trusted_hash") val twoFaHash: String?, - @SerializedName("validation_sid") val validationSid: String?, - @SerializedName("validation_type") val validationType: String?, - @SerializedName("phone_mask") val phoneMask: String?, - @SerializedName("redirect_uri") val redirectUrl: String?, - @SerializedName("validation_resend") val validationResend: String?, - @SerializedName("cant_get_code_open_restore") val isCanNotGetCodeNeedToOpenRestore: Boolean -) : Parcelable - -@Parcelize -data class SendSmsResponse( - @SerializedName("sid") val validationSid: String?, - @SerializedName("delay") val delay: Int?, - @SerializedName("validation_type") val validationType: String?, - @SerializedName("validation_resend") val validationResend: String? -) : Parcelable diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthUrls.kt deleted file mode 100644 index 1a888435..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthUrls.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.meloda.fast.api.network.auth - -import com.meloda.fast.api.network.VkUrls - -object AuthUrls { - - const val DirectAuth = "${VkUrls.OAUTH}/token" - const val SendSms = "${VkUrls.API}/auth.validatePhone" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt deleted file mode 100644 index c8f461a4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.network.conversations - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import com.meloda.fast.api.model.data.BaseVkConversation -import com.meloda.fast.api.model.base.BaseVkGroup -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.model.base.BaseVkUser -import kotlinx.parcelize.Parcelize - -@Parcelize -data class ConversationsGetResponse( - val count: Int, - val items: List, - @SerializedName("unread_count") - val unreadCount: Int?, - val profiles: List?, - val groups: List? -) : Parcelable - -@Parcelize -data class ConversationsResponseItems( - val conversation: BaseVkConversation, - @SerializedName("last_message") - val lastMessage: BaseVkMessage? -) : Parcelable diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsUrls.kt deleted file mode 100644 index 17715d2f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsUrls.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.network.conversations - -import com.meloda.fast.api.network.VkUrls - -object ConversationsUrls { - - const val Get = "${VkUrls.API}/messages.getConversations" - const val Delete = "${VkUrls.API}/messages.deleteConversation" - const val Pin = "${VkUrls.API}/messages.pinConversation" - const val Unpin = "${VkUrls.API}/messages.unpinConversation" - const val ReorderPinned = "${VkUrls.API}/messages.reorderPinnedConversations" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/files/FileRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/files/FileRequests.kt deleted file mode 100644 index a94949af..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/files/FileRequests.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.network.files - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesResponses.kt deleted file mode 100644 index abef98ae..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesResponses.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.meloda.fast.api.network.files - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import com.meloda.fast.api.model.base.attachments.BaseVkFile -import com.meloda.fast.api.model.base.attachments.BaseVkVoiceMessage -import kotlinx.parcelize.Parcelize - -@Parcelize -data class FilesGetMessagesUploadServerResponse( - @SerializedName("upload_url") - val uploadUrl: String -) : Parcelable - -@Parcelize -data class FilesUploadFileResponse(val file: String?, val error: String?) : Parcelable - -@Parcelize -data class FilesSaveFileResponse( - val type: String, - @SerializedName("doc") - val file: BaseVkFile?, - @SerializedName("audio_message") - val voiceMessage: BaseVkVoiceMessage? -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesUrls.kt deleted file mode 100644 index 1282e234..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesUrls.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.api.network.files - -import com.meloda.fast.api.network.VkUrls - -object FilesUrls { - - const val GetMessagesUploadServer = "${VkUrls.API}/docs.getMessagesUploadServer" - - const val Save = "${VkUrls.API}/docs.save" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt deleted file mode 100644 index 86dd9a6a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.meloda.fast.api.network.messages - -import android.os.Parcelable -import com.meloda.fast.api.model.base.* -import com.meloda.fast.api.model.data.BaseVkConversation -import kotlinx.parcelize.Parcelize - -@Parcelize -data class MessagesGetHistoryResponse( - val count: Int, - val items: List = emptyList(), - val conversations: List?, - val profiles: List?, - val groups: List? -) : Parcelable - -@Parcelize -data class MessagesGetByIdResponse( - val count: Int, - val items: List = emptyList(), - val profiles: List?, - val groups: List? -) : Parcelable - -@Parcelize -data class MessagesGetConversationMembersResponse( - val count: Int, - val items: List = emptyList(), - val profiles: List?, - val groups: List? -) : Parcelable diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesUrls.kt deleted file mode 100644 index 829c76c6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesUrls.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.meloda.fast.api.network.messages - -import com.meloda.fast.api.network.VkUrls - -object MessagesUrls { - - const val GetHistory = "${VkUrls.API}/messages.getHistory" - const val Send = "${VkUrls.API}/messages.send" - const val MarkAsImportant = "${VkUrls.API}/messages.markAsImportant" - const val GetLongPollServer = "${VkUrls.API}/messages.getLongPollServer" - const val GetLongPollHistory = "${VkUrls.API}/messages.getLongPollHistory" - const val Pin = "${VkUrls.API}/messages.pin" - const val Unpin = "${VkUrls.API}/messages.unpin" - const val Delete = "${VkUrls.API}/messages.delete" - const val Edit = "${VkUrls.API}/messages.edit" - const val GetById = "${VkUrls.API}/messages.getById" - const val MarkAsRead = "${VkUrls.API}/messages.markAsRead" - const val GetChat = "${VkUrls.API}/messages.getChat" - const val GetConversationMembers = "${VkUrls.API}/messages.getConversationMembers" - const val RemoveChatUser = "${VkUrls.API}/messages.removeChatUser" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaResponses.kt deleted file mode 100644 index fd0c6961..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaResponses.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.api.network.ota - -import android.os.Parcelable -import com.meloda.fast.model.UpdateItem -import kotlinx.parcelize.Parcelize - -@Parcelize -data class OtaGetLatestReleaseResponse(val release: UpdateItem?) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaUrls.kt deleted file mode 100644 index 473d0ea9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaUrls.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.api.network.ota - -object OtaUrls { - - const val GetActualUrl = - "https://raw.githubusercontent.com/melod1n/ota-server/master/ngrok_url.json" -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotoUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotoUrls.kt deleted file mode 100644 index 05b897e5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotoUrls.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.api.network.photos - -import com.meloda.fast.api.network.VkUrls - -object PhotoUrls { - - const val GetMessagesUploadServer = "${VkUrls.API}/photos.getMessagesUploadServer" - - const val SaveMessagePhoto = "${VkUrls.API}/photos.saveMessagesPhoto" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosRequests.kt deleted file mode 100644 index 8e457ff0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosRequests.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.meloda.fast.api.network.photos - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class PhotosSaveMessagePhotoRequest( - val photo: String, val server: Int, val hash: String -) : Parcelable { - val map - get() = mapOf( - "photo" to photo, - "server" to server.toString(), - "hash" to hash - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosResponses.kt deleted file mode 100644 index c9a31973..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosResponses.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.api.network.photos - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class PhotosGetMessagesUploadServerResponse( - @SerializedName("album_id") - val albumId: Int, - @SerializedName("upload_url") - val uploadUrl: String -) : Parcelable - -@Parcelize -data class PhotosUploadPhotoResponse( - val server: Int, val photo: String, val hash: String -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt deleted file mode 100644 index c4084e58..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.network.users - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersUrls.kt deleted file mode 100644 index 64761573..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersUrls.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.api.network.users - -import com.meloda.fast.api.network.VkUrls - -object UsersUrls { - - const val GetById = "${VkUrls.API}/users.get" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosRequests.kt deleted file mode 100644 index 1f196662..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosRequests.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.network.videos - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosResponses.kt deleted file mode 100644 index 86264318..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosResponses.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.meloda.fast.api.network.videos - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VideosSaveResponse( - @SerializedName("access_key") - val accessKey: String, - val description: String, - @SerializedName("owner_id") - val ownerId: Int, - val title: String, - @SerializedName("upload_url") - val uploadUrl: String, - @SerializedName("video_id") - val videoId: Int -) : Parcelable { - -} - -@Parcelize -data class VideosUploadResponse( - @SerializedName("video_hash") - val hash: String?, - val size: Int, - @SerializedName("direct_link") - val directLink: String, - @SerializedName("owner_id") - val ownerId: Int, - @SerializedName("video_id") - val videoId: Int, - val error: String? -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosUrls.kt deleted file mode 100644 index c2cc9308..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosUrls.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.api.network.videos - -import com.meloda.fast.api.network.VkUrls - -object VideosUrls { - - const val Save = "${VkUrls.API}/video.save" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt deleted file mode 100644 index 3113c485..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.base - -import androidx.annotation.LayoutRes -import androidx.appcompat.app.AppCompatActivity - -abstract class BaseActivity : AppCompatActivity { - - constructor() : super() - - constructor(@LayoutRes resId: Int) : super(resId) -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseFragment.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseFragment.kt deleted file mode 100644 index 20d217db..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseFragment.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.base - -import androidx.annotation.LayoutRes -import androidx.fragment.app.Fragment - -abstract class BaseFragment : Fragment { - - constructor() : super() - - constructor(@LayoutRes resId: Int) : super(resId) -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/ResourceProvider.kt b/app/src/main/kotlin/com/meloda/fast/base/ResourceProvider.kt deleted file mode 100644 index f2ab5f99..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/ResourceProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.base - -import android.content.Context -import android.graphics.drawable.Drawable -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.core.content.ContextCompat - -abstract class ResourceProvider(protected val context: Context) { - - protected fun getString(@StringRes resId: Int): String { - return context.getString(resId) - } - - @ColorInt - protected fun getColor(@ColorRes resId: Int): Int { - return ContextCompat.getColor(context, resId) - } - - protected fun getDrawable(@DrawableRes resId: Int): Drawable? { - return ContextCompat.getDrawable(context, resId) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/AsyncDiffItemAdapter.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/AsyncDiffItemAdapter.kt deleted file mode 100644 index 9167b253..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/AsyncDiffItemAdapter.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.meloda.fast.base.adapter - -import androidx.recyclerview.widget.DiffUtil -import com.hannesdorfmann.adapterdelegates4.AdapterDelegate -import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import com.meloda.fast.model.base.AdapterDiffItem - -class AsyncDiffItemAdapter( - customDiffCallback: DiffUtil.ItemCallback? = null, - vararg delegates: AdapterDelegate>, -) : AsyncListDifferDelegationAdapter(customDiffCallback ?: DIFF_CALLBACK) { - - constructor( - vararg delegates: AdapterDelegate>, - ) : this(customDiffCallback = null) { - delegates.forEach(::addDelegate) - } - - init { - delegates.forEach(::addDelegate) - } - - fun addDelegates(vararg delegates: AdapterDelegate>) { - delegates.forEach(::addDelegate) - } - - @Suppress("UNCHECKED_CAST") - fun addDelegate(delegate: AdapterDelegate>) { - (delegate as? AdapterDelegate>)?.let(delegatesManager::addDelegate) - } - - fun isEmpty() = itemCount == 0 - fun isNotEmpty() = itemCount > 0 - - companion object { - val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: AdapterDiffItem, - newItem: AdapterDiffItem, - ): Boolean { - return oldItem.areItemsTheSame(newItem) - } - - override fun areContentsTheSame( - oldItem: AdapterDiffItem, - newItem: AdapterDiffItem, - ): Boolean { - return oldItem.areContentsTheSame(newItem) - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt deleted file mode 100644 index 22ac45b8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt +++ /dev/null @@ -1,281 +0,0 @@ -package com.meloda.fast.base.adapter - -import android.content.Context -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.widget.AdapterView -import android.widget.Filter -import android.widget.Filterable -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import kotlinx.coroutines.* -import kotlin.properties.Delegates - -@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST") -abstract class BaseAdapter constructor( - var context: Context, - diffUtil: DiffUtil.ItemCallback, - preAddedValues: List = emptyList(), -) : ListAdapter(diffUtil), Filterable { - - private var valuesFilter: ValuesFilter? = null - - protected val adapterScope = CoroutineScope(Dispatchers.Default) - private val cleanList = mutableListOf() - - protected var inflater: LayoutInflater = LayoutInflater.from(context) - - var itemClickListener: ((position: Int) -> Unit)? = null - var itemLongClickListener: ((position: Int) -> Boolean)? = null - - private val listForSave = mutableListOf() - - var isSearching: Boolean by Delegates.observable(false) { _, _, _ -> - updateSearchingState() - } - - init { - cleanList.addAll(preAddedValues) - addAll(preAddedValues) - } - - fun cloneCurrentList(): MutableList { - return currentList.toMutableList() - } - - open fun destroy() {} - - fun getOrNull(position: Int): T? { - return if (position >= 0 && position <= currentList.lastIndex) get(position) else null - } - - fun getOrElse(position: Int, defaultValue: (Int) -> T): T { - return if (position >= 0 && position <= currentList.lastIndex) get(position) - else defaultValue(position) - } - - fun add( - item: T, - position: Int? = null, - commitCallback: (() -> Unit)? = null - ) = addAll(listOf(item), position, commitCallback) - - fun addAll( - items: List, - position: Int? = null, - commitCallback: (() -> Unit)? = null - ) { - adapterScope.launch { - val newList = cloneCurrentList() - if (position == null) { - val mutableItems = items.toMutableList() - - newList.addAll(mutableItems) - cleanList.addAll(mutableItems) - } else { - newList.addAll(position, items) - cleanList.addAll(position, items) - } - - withContext(Dispatchers.Main) { - submitList(newList, commitCallback) - } - } - } - - fun remove(item: T, commitCallback: (() -> Unit)? = null) = - removeAll(listOf(item), commitCallback) - - fun removeAll(items: List, commitCallback: (() -> Unit)? = null) { - val newList = cloneCurrentList() - newList.removeAll(items) - cleanList.removeAll(items) - - submitList(newList, commitCallback) - } - - fun removeAt(index: Int, commitCallback: (() -> Unit)? = null) { - val newList = cloneCurrentList() - newList.removeAt(index) - cleanList.removeAt(index) - - submitList(newList, commitCallback) - } - - fun clear(commitCallback: (() -> Unit)? = null) = removeAll(currentList, commitCallback) - - fun setItem( - item: T, - commitCallback: (() -> Unit)? = null - ) = setItems(listOf(item), commitCallback) - - @Suppress("UNCHECKED_CAST") - fun setItems( - list: List?, - commitCallback: (() -> Unit)? = null - ) { - adapterScope.launch { - val items = mutableListOf() - if (!list.isNullOrEmpty()) items.addAll(list) - - withContext(Dispatchers.Main) { - if (items == currentList) { - refreshList() - } else { - submitList(items, commitCallback) - } - } - } - } - - fun indexOf(item: T): Int { - return currentList.indexOf(item) - } - - fun searchIndexOf(item: T): Int? { - val index = indexOf(item) - return if (index == -1) null else index - } - - val indices get() = currentList.indices - - operator fun get(position: Int): T { - return currentList[position] - } - - operator fun set(position: Int, item: T) = setItem(position, item) - - fun setItem(position: Int, item: T, commitCallback: (() -> Unit)? = null) { - val newList = cloneCurrentList() - newList[position] = item - cleanList[position] = item - - submitList(newList, commitCallback) - } - - fun isEmpty() = currentList.isEmpty() - fun isNotEmpty() = currentList.isNotEmpty() - - fun refreshList() { - notifyItemRangeChanged(0, itemCount) - } - - fun updateCleanList(list: List?) { - cleanList.clear() - list?.run { cleanList.addAll(this) } - } - - override fun submitList(list: List?) { - super.submitList(list) - updateCleanList(list) - } - - override fun submitList(list: List?, commitCallback: Runnable?) { - super.submitList(list, commitCallback) - updateCleanList(list) - } - - override fun onBindViewHolder(holder: VH, position: Int) { - initListeners(holder.itemView, position) - holder.bind(position) - } - - protected open fun initListeners(itemView: View, position: Int) { - if (itemView is AdapterView<*>) return - - itemView.setOnClickListener { itemClickListener?.invoke(position) } - itemView.setOnLongClickListener { - itemLongClickListener?.invoke(position) - return@setOnLongClickListener itemClickListener != null - } - } - - override fun getItemCount(): Int { - return currentList.size - } - - val lastPosition get() = currentList.lastIndex - - private fun updateSearchingState() { - Log.d("BaseAdapter", "updateSearchingState: $isSearching") - - cleanList.clear() - - if (isSearching) { - listForSave.clear() - listForSave += cloneCurrentList() - } else { - setItems(listForSave, commitCallback = { - listForSave.clear() - }) - } - } - - open fun filter(query: String) { - if (cleanList.isEmpty()) { - cleanList.addAll(listForSave) - } - - val newList = mutableListOf() - - setItems(emptyList(), commitCallback = { - if (query.isEmpty()) { - newList.addAll(cleanList) - } else { - for (item in cleanList) { - if (onQueryItem(item, query)) { - newList.add(item) - } - } - } - - setItems(newList) - }) - } - - open fun onQueryItem(item: T, query: String): Boolean { - return false - } - - override fun getFilter(): Filter { - if (valuesFilter == null) { - valuesFilter = ValuesFilter() - } - - return requireNotNull(valuesFilter) - } - - private inner class ValuesFilter : Filter() { - override fun performFiltering(constraint: CharSequence?): FilterResults { - val results = FilterResults() - - if (isEmpty()) return results - - if (!constraint.isNullOrEmpty()) { - val filteredList = mutableListOf() - for (item in listForSave) { - if (onQueryItem(item, constraint.toString())) { - filteredList.add(item) - } - } - results.count = filteredList.size - results.values = filteredList - } else { - results.count = listForSave.size - results.values = listForSave - } - - return results - } - - override fun publishResults(constraint: CharSequence?, results: FilterResults) { - val items = results.values as? List - setItems(items) - } - } - - override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) { - super.onCurrentListChanged(previousList, currentList) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/Holders.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/Holders.kt deleted file mode 100644 index df2f0d63..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/Holders.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.base.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) { - - open fun bind(position: Int) { - bind(position, null) - } - - open fun bind(position: Int, payloads: MutableList?) {} - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/Listeners.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/Listeners.kt deleted file mode 100644 index 204ec669..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/Listeners.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.base.adapter - -fun interface OnItemClickListener { - fun onItemClick(item: T) -} - -fun interface OnItemLongClickListener { - fun onLongItemClick(item: T): Boolean -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/screen/AppScreen.kt b/app/src/main/kotlin/com/meloda/fast/base/screen/AppScreen.kt deleted file mode 100644 index 2fd269af..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/screen/AppScreen.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.meloda.fast.base.screen - -import com.github.terrakok.cicerone.Router -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow - -interface AppScreen { - val resultFlow: MutableSharedFlow - - var args: ArgType - - fun show(router: Router, args: ArgType) - - fun getArguments(): ArgType = args -} - -@Suppress("unused") -fun AppScreen.createResultFlow(): MutableSharedFlow { - return MutableSharedFlow( - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt deleted file mode 100644 index 9509b8c4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import androidx.lifecycle.ViewModel -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.network.* -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.notNull - -abstract class BaseViewModel : ViewModel() { - - open suspend fun sendSingleEvent(event: VkEvent) {} - - suspend fun sendRequestNotNull( - onError: ErrorHandler? = null, - request: suspend () -> ApiAnswer - ): T = sendRequest(onError, request).notNull() - - suspend fun sendRequest( - onError: ErrorHandler? = null, - request: suspend () -> ApiAnswer, - ): T? { - return when (val response = request()) { - is ApiAnswer.Success -> response.data - is ApiAnswer.Error -> { - val error = response.error - - if (!onError?.handleError(error).isTrue) { - checkErrors(error) - } - - null - } - } - } - - protected suspend fun checkErrors(throwable: Throwable) { - when (throwable) { - is TokenExpiredError -> { - sendSingleEvent(TokenExpiredErrorEvent) - } - is AuthorizationError -> { - sendSingleEvent(AuthorizationErrorEvent) - } - is UserBannedError -> { - throwable.banInfo.let { banInfo -> - sendSingleEvent( - UserBannedEvent( - memberName = banInfo.memberName, - message = banInfo.message, - restoreUrl = banInfo.restoreUrl, - accessToken = banInfo.accessToken - ) - ) - } - } - is ValidationRequiredError -> { - sendSingleEvent( - ValidationRequiredEvent( - sid = throwable.validationSid, - redirectUri = throwable.redirectUri, - phoneMask = throwable.phoneMask, - validationType = throwable.validationType, - canResendSms = throwable.validationResend == "sms", - codeError = null - ) - ) - } - is CaptchaRequiredError -> { - sendSingleEvent( - CaptchaRequiredEvent( - sid = throwable.captchaSid, - image = throwable.captchaImg - ) - ) - } - - is ApiError -> { - sendSingleEvent( - if (throwable.errorMessage == null) { - UnknownErrorEvent - } else { - ErrorTextEvent(errorText = requireNotNull(throwable.errorMessage)) - } - ) - } - else -> { - sendSingleEvent( - if (throwable.message == null) { - UnknownErrorEvent - } else { - ErrorTextEvent(requireNotNull(throwable.message)) - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModelFragment.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModelFragment.kt deleted file mode 100644 index cb9f8b38..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModelFragment.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import android.os.Bundle -import android.view.View -import androidx.annotation.LayoutRes -import androidx.lifecycle.lifecycleScope -import com.meloda.fast.base.BaseFragment -import kotlinx.coroutines.launch - -@Deprecated("", ReplaceWith("BaseFragment")) -abstract class BaseViewModelFragment : BaseFragment { - - constructor() : super() - - constructor(@LayoutRes resId: Int) : super(resId) - - protected abstract val viewModel: VM - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - subscribeToViewModel(viewModel) - } - - protected open fun onEvent(event: VkEvent) { - ViewModelUtils.parseEvent(this, event) - } - - protected fun subscribeToViewModel(viewModel: T) { - lifecycleScope.launch { - viewModel.tasksEvent.collect { onEvent(it) } - } - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/DeprecatedBaseViewModel.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/DeprecatedBaseViewModel.kt deleted file mode 100644 index b88206ac..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/DeprecatedBaseViewModel.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.network.* -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.notNull -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -@Deprecated("rewrite") -abstract class DeprecatedBaseViewModel : ViewModel() { - - private val tasksEventChannel = Channel() - val tasksEvent = tasksEventChannel.receiveAsFlow() - - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - viewModelScope.launch { onException(throwable) } - } - - fun launch(block: suspend CoroutineScope.() -> Unit): Job { - return viewModelScope.launch(exceptionHandler, block = block) - } - - suspend fun sendRequestNotNull( - onError: ErrorHandler? = null, - request: suspend () -> ApiAnswer - ): T = sendRequest(onError, request).notNull() - - suspend fun sendRequest( - onError: ErrorHandler? = null, - request: suspend () -> ApiAnswer, - ): T? { - return when (val response = request()) { - is ApiAnswer.Success -> response.data - is ApiAnswer.Error -> { - val error = response.error - - if (!onError?.handleError(error).isTrue) { - checkErrors(error) - } - - null - } - } - } - - // TODO: 05.04.2023, Danil Nikolaev: переписать makeJob на sendRequest (oh boy, писать дохуя) - // TODO: 05.04.2023, Danil Nikolaev: переписать Conversations Screen на новую архитектуру, пока что оставить View - - protected fun makeJob( - job: suspend () -> ApiAnswer, - onAnswer: suspend (T) -> Unit = {}, - onStart: (suspend () -> Unit)? = null, - onEnd: (suspend () -> Unit)? = null, - onError: (suspend (Throwable) -> Unit)? = null, - onAnyResult: (suspend () -> Unit)? = null, - ): Job = viewModelScope.launch { - onStart?.invoke() - when (val response = job()) { - is ApiAnswer.Success -> { - onAnswer(response.data) - onAnyResult?.invoke() - } - is ApiAnswer.Error -> { - onError?.invoke(response.error) ?: checkErrors(response.error) - onAnyResult?.invoke() - } - } - }.also { - it.invokeOnCompletion { - viewModelScope.launch { - onEnd?.invoke() - } - } - } - - protected open suspend fun onException(throwable: Throwable) { - checkErrors(throwable) - } - - protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) - - protected suspend fun checkErrors(throwable: Throwable) { - when (throwable) { - is TokenExpiredError -> sendEvent(TokenExpiredErrorEvent) - is AuthorizationError -> sendEvent(AuthorizationErrorEvent) - is UserBannedError -> { - val banInfo = throwable.banInfo - sendEvent( - UserBannedEvent( - memberName = banInfo.memberName, - message = banInfo.message, - restoreUrl = banInfo.restoreUrl, - accessToken = banInfo.accessToken - ) - ) - } - is ValidationRequiredError -> { - sendEvent( - ValidationRequiredEvent( - sid = throwable.validationSid, - redirectUri = throwable.redirectUri, - phoneMask = throwable.phoneMask, - validationType = throwable.validationType, - canResendSms = throwable.validationResend == "sms", - codeError = null - ) - ) - } - is CaptchaRequiredError -> sendEvent( - CaptchaRequiredEvent( - sid = throwable.captchaSid, - image = throwable.captchaImg - ) - ) - - is ApiError -> sendEvent( - if (throwable.errorMessage == null) { - UnknownErrorEvent - } else { - ErrorTextEvent(errorText = requireNotNull(throwable.errorMessage)) - } - ) - else -> sendEvent( - if (throwable.message == null) { - UnknownErrorEvent - } else { - ErrorTextEvent(requireNotNull(throwable.message)) - } - ) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ErrorHandler.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ErrorHandler.kt deleted file mode 100644 index aa702965..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ErrorHandler.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.base.viewmodel - -fun interface ErrorHandler { - - /** - * @return true if error has been handled manually - */ - suspend fun handleError(error: Throwable): Boolean -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt deleted file mode 100644 index 02acae53..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import com.meloda.fast.model.base.UiText - -abstract class VkEvent - -abstract class VkErrorEvent(open val errorText: String? = null) : VkEvent() - -object UnknownErrorEvent : VkErrorEvent() -open class ErrorTextEvent(override val errorText: String) : VkErrorEvent() - -object AuthorizationErrorEvent : VkErrorEvent() -object TokenExpiredErrorEvent : VkErrorEvent() -data class CaptchaRequiredEvent(val sid: String, val image: String) : VkErrorEvent() -data class ValidationRequiredEvent( - val sid: String, - val redirectUri: String, - val phoneMask: String, - val validationType: String, - val canResendSms: Boolean, - val codeError: UiText? -) : VkErrorEvent() - -data class UserBannedEvent( - val memberName: String, val message: String, val restoreUrl: String, val accessToken: String, -) : VkErrorEvent() - -fun interface VkEventCallback { - fun onEvent(event: T) -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ViewModelUtils.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ViewModelUtils.kt deleted file mode 100644 index 716c143f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ViewModelUtils.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import android.content.Intent -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.ext.showDialog -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.main.activity.MainActivity - -object ViewModelUtils { - - @Deprecated("rewrite") - @Suppress("MemberVisibilityCanBePrivate") - fun parseEvent(activity: FragmentActivity, event: VkEvent) { - when (event) { - is AuthorizationErrorEvent -> { - Toast.makeText( - activity, R.string.authorization_failed, Toast.LENGTH_LONG - ).show() - - UserConfig.clear() - activity.finishAffinity() - activity.startActivity(Intent(activity, MainActivity::class.java)) - } - is TokenExpiredErrorEvent -> { - Toast.makeText( - activity, R.string.token_expired, Toast.LENGTH_LONG - ).show() - - UserConfig.clear() - activity.finishAffinity() - activity.startActivity(Intent(activity, MainActivity::class.java)) - } - is UserBannedEvent -> { - // TODO: 17.04.2023, Danil Nikolaev: handle banned event -// (activity as? MainActivity)?.accessRouter()?.newRootScreen( -// Screens.UserBanned( -// memberName = event.memberName, -// message = event.message, -// restoreUrl = event.restoreUrl, -// accessToken = event.accessToken -// ) -// ) - } - is UnknownErrorEvent -> { - activity.showDialog( - title = UiText.Resource(R.string.title_error), - message = UiText.Resource(R.string.unknown_error_occurred), - positiveText = UiText.Resource(R.string.ok) - ) - } - is VkErrorEvent -> { - event.errorText?.run { - activity.showDialog( - title = UiText.Resource(R.string.title_error), - message = UiText.Simple(this), - positiveText = UiText.Resource(R.string.ok) - ) - } - } - } - } - - @Deprecated("rewrite") - fun parseEvent(fragment: Fragment, event: VkEvent) { - parseEvent(fragment.requireActivity(), event) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/common/AppConstants.kt b/app/src/main/kotlin/com/meloda/fast/common/AppConstants.kt deleted file mode 100644 index 62d545a7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/AppConstants.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.common - -object AppConstants { - - const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt b/app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt deleted file mode 100644 index e8c3b4f8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.meloda.fast.common - -import android.app.Application -import android.content.Context -import android.content.SharedPreferences -import android.content.pm.PackageManager -import android.content.res.Resources -import android.media.AudioManager -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.pm.PackageInfoCompat -import androidx.preference.PreferenceManager -import com.google.android.material.color.DynamicColors -import com.meloda.fast.common.di.applicationModule -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.AndroidUtils -import org.koin.android.ext.koin.androidContext -import org.koin.android.ext.koin.androidLogger -import org.koin.core.context.GlobalContext.startKoin -import kotlin.math.roundToInt -import kotlin.properties.Delegates - -class AppGlobal : Application() { - - override fun onCreate() { - super.onCreate() - - instance = this - - if (preferences.getBoolean( - SettingsFragment.KEY_USE_DYNAMIC_COLORS, - SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - ) { - DynamicColors.applyToActivitiesIfAvailable(this) - } - - val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES) - versionName = info.versionName - versionCode = PackageInfoCompat.getLongVersionCode(info).toInt() - - screenWidth80 = (AndroidUtils.getDisplayWidth() * 0.8).roundToInt() - - audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - - applyDarkTheme() - - initKoin() - } - - private fun applyDarkTheme() { - val nightMode = preferences.getInt( - SettingsFragment.KEY_APPEARANCE_DARK_THEME, - SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME - ) - AppCompatDelegate.setDefaultNightMode(nightMode) - } - - private fun initKoin() { - startKoin { - androidLogger() - androidContext(this@AppGlobal) - modules(applicationModule) - } - } - - companion object { - private lateinit var instance: AppGlobal - - val preferences: SharedPreferences by lazy { - PreferenceManager.getDefaultSharedPreferences(instance) - } - - var versionName = "" - var versionCode = 0 - var screenWidth80 = 0 - - val Instance: AppGlobal get() = instance - val resources: Resources get() = Instance.resources - val packageManager: PackageManager get() = Instance.packageManager - - var audioManager: AudioManager by Delegates.notNull() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/common/Screens.kt b/app/src/main/kotlin/com/meloda/fast/common/Screens.kt deleted file mode 100644 index b0719a89..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/Screens.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.meloda.fast.common - -import com.github.terrakok.cicerone.androidx.FragmentScreen -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.domain.VkConversationDomain -import com.meloda.fast.model.UpdateItem -import com.meloda.fast.screens.chatinfo.ChatInfoFragment -import com.meloda.fast.screens.conversations.ConversationsFragment -import com.meloda.fast.screens.login.LoginFragment -import com.meloda.fast.screens.main.MainFragment -import com.meloda.fast.screens.messages.ForwardedMessagesFragment -import com.meloda.fast.screens.messages.MessagesHistoryFragment -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.screens.updates.UpdatesFragment -import com.meloda.fast.screens.userbanned.UserBannedFragment - -@Suppress("FunctionName") -object Screens { - fun Main() = FragmentScreen { MainFragment.newInstance() } - - fun Login() = FragmentScreen { LoginFragment.newInstance() } - - fun Conversations() = FragmentScreen { ConversationsFragment() } - - fun MessagesHistory( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup? - ) = FragmentScreen { MessagesHistoryFragment.newInstance(conversation, user, group) } - - fun ForwardedMessages( - conversation: VkConversationDomain, - messages: List, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf() - ) = FragmentScreen { - ForwardedMessagesFragment.newInstance( - conversation, messages, profiles, groups - ) - } - - fun ChatInfo( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup? - ) = FragmentScreen { ChatInfoFragment.newInstance(conversation, user, group) } - - fun Updates(updateItem: UpdateItem? = null) = - FragmentScreen { UpdatesFragment.newInstance(updateItem) } - - fun Settings() = FragmentScreen { SettingsFragment.newInstance() } - - fun UserBanned( - memberName: String, - message: String, - restoreUrl: String, - accessToken: String - ) = FragmentScreen { - UserBannedFragment.newInstance( - memberName, message, restoreUrl, accessToken - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/common/UpdateManager.kt b/app/src/main/kotlin/com/meloda/fast/common/UpdateManager.kt deleted file mode 100644 index 5ea0d868..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/UpdateManager.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.meloda.fast.common - -import com.meloda.fast.BuildConfig -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse -import com.meloda.fast.data.ota.OtaApi -import com.meloda.fast.model.UpdateActualUrl -import com.meloda.fast.model.UpdateItem -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import java.net.URLEncoder -import kotlin.coroutines.CoroutineContext - -interface UpdateManager { - val stateFlow: Flow - - fun checkUpdates(): Job -} - -class UpdateManagerImpl(private val repo: OtaApi) : UpdateManager { - - private val coroutineContext: CoroutineContext - get() = Dispatchers.IO - - private val coroutineScope = CoroutineScope(coroutineContext) - - private var otaBaseUrl: String? = null - - override val stateFlow = MutableStateFlow(UpdateManagerState.EMPTY) - - override fun checkUpdates() = coroutineScope.launch { - val job: suspend () -> ApiAnswer = { repo.getActualUrl() } - - when (val jobResponse = job()) { - is ApiAnswer.Success -> { - val item = jobResponse.data - otaBaseUrl = item.url - - getLatestRelease() - } - is ApiAnswer.Error -> { - otaBaseUrl = null - val throwable = jobResponse.error.throwable - - val newForm = stateFlow.value.copy( - updateItem = null, - throwable = throwable - ) - stateFlow.emit(newForm) - } - } - } - - private fun getLatestRelease() = coroutineScope.launch { - val url = "$otaBaseUrl/releases-latest" - - val job: suspend () -> ApiAnswer> = { - repo.getLatestRelease(url = url, secretCode = getOtaSecret()) - } - - when (val jobResponse = job()) { - is ApiAnswer.Success -> { - val response = jobResponse.data.response ?: return@launch - val latestRelease = response.release - - val updateItem = if (latestRelease != null && - (AppGlobal.versionName - .split("_") - .getOrNull(1) != latestRelease.versionName || - AppGlobal.versionCode < latestRelease.versionCode) - ) { - latestRelease - } else { - null - } - - val newForm = stateFlow.value.copy( - updateItem = updateItem, - throwable = null - ) - - stateFlow.emit(newForm) - } - - is ApiAnswer.Error -> { - val throwable = jobResponse.error.throwable - - val newForm = stateFlow.value.copy( - updateItem = null, - throwable = throwable - ) - stateFlow.emit(newForm) - } - } - } - - private fun getOtaSecret(): String { - return URLEncoder.encode(BuildConfig.otaSecretCode, "utf-8") - } -} - -data class UpdateManagerState( - val updateItem: UpdateItem?, - val throwable: Throwable?, -) { - companion object { - val EMPTY = UpdateManagerState( - updateItem = null, throwable = null - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/common/di/ApplicationModule.kt b/app/src/main/kotlin/com/meloda/fast/common/di/ApplicationModule.kt deleted file mode 100644 index 58784bac..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/di/ApplicationModule.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.meloda.fast.common.di - -import com.meloda.fast.di.apiModule -import com.meloda.fast.di.dataModule -import com.meloda.fast.di.databaseModule -import com.meloda.fast.di.navigationModule -import com.meloda.fast.di.networkModule -import com.meloda.fast.di.otaModule -import com.meloda.fast.screens.captcha.di.captchaModule -import com.meloda.fast.screens.chatinfo.di.chatInfoModule -import com.meloda.fast.screens.conversations.di.conversationsModule -import com.meloda.fast.screens.login.di.loginModule -import com.meloda.fast.screens.main.di.mainModule -import com.meloda.fast.screens.messages.di.messagesHistoryModule -import com.meloda.fast.screens.photos.di.photoViewModule -import com.meloda.fast.screens.settings.di.settingsModule -import com.meloda.fast.screens.twofa.di.twoFaModule -import com.meloda.fast.screens.updates.di.updatesModule -import org.koin.dsl.module - -val applicationModule = module { - includes( - navigationModule, - databaseModule, - dataModule, - otaModule, - networkModule, - apiModule, - loginModule, - twoFaModule, - captchaModule, - mainModule, - conversationsModule, - chatInfoModule, - settingsModule, - updatesModule, - messagesHistoryModule, - photoViewModule, - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/compose/Dialogs.kt b/app/src/main/kotlin/com/meloda/fast/compose/Dialogs.kt deleted file mode 100644 index 4f03a816..00000000 --- a/app/src/main/kotlin/com/meloda/fast/compose/Dialogs.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.meloda.fast.compose - -import androidx.compose.animation.* -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import com.meloda.fast.ext.getString -import com.meloda.fast.model.base.UiText -import com.meloda.fast.ui.AppTheme - -@Composable -fun MaterialDialog( - onDismissAction: (() -> Unit), - title: UiText? = null, - message: UiText? = null, - positiveText: UiText? = null, - positiveAction: (() -> Unit)? = null, - negativeText: UiText? = null, - negativeAction: (() -> Unit)? = null, - neutralText: UiText? = null, - neutralAction: (() -> Unit)? = null, - content: (@Composable () -> Unit)? = null -) { - var isVisible by remember { - mutableStateOf(true) - } - val onDismissRequest = { - onDismissAction.invoke() - isVisible = false - } - - AppTheme { - // TODO: 08.04.2023, Danil Nikolaev: implement animation - AlertAnimation(visible = isVisible) { - Dialog(onDismissRequest = onDismissRequest) { - val scrollState = rememberScrollState() - val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } } - val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } } - - Surface( - modifier = Modifier.fillMaxWidth(), - color = AlertDialogDefaults.containerColor, - shape = AlertDialogDefaults.shape, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column( - modifier = Modifier.padding( - start = 20.dp, - top = 20.dp, - end = 20.dp, - bottom = 10.dp - ) - ) { - Row { - title?.getString()?.let { title -> - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = title, - style = MaterialTheme.typography.headlineSmall - ) - } - } - - if (canScrollBackward) { - Divider(modifier = Modifier.fillMaxWidth()) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f, fill = false) - .verticalScroll(scrollState) - ) { - Spacer(modifier = Modifier.height(8.dp)) - Row { - message?.getString()?.let { message -> - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - content?.let { content -> - Spacer(modifier = Modifier.height(4.dp)) - content.invoke() - Spacer(modifier = Modifier.height(10.dp)) - } - } - - if (canScrollForward) { - Divider(modifier = Modifier.fillMaxWidth()) - } - - Row { - neutralText?.getString()?.let { text -> - TextButton( - onClick = { - onDismissRequest.invoke() - neutralAction?.invoke() - } - ) { - Text(text = text) - } - } - - Spacer(modifier = Modifier.weight(1f)) - - negativeText?.getString()?.let { text -> - TextButton( - onClick = { - onDismissRequest.invoke() - negativeAction?.invoke() - } - ) { - Text(text = text) - } - } - - Spacer(modifier = Modifier.width(2.dp)) - - positiveText?.getString()?.let { text -> - TextButton( - onClick = { - onDismissRequest.invoke() - positiveAction?.invoke() - } - ) { - Text(text = text) - } - } - } - } - } - } - } - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun AlertAnimation( - visible: Boolean, - content: @Composable AnimatedVisibilityScope.() -> Unit -) { - AnimatedVisibility( - visible = visible, - enter = fadeIn(animationSpec = tween(400)) + - scaleIn(animationSpec = tween(400)), - exit = fadeOut(animationSpec = tween(150)), - content = content - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/account/AccountApi.kt b/app/src/main/kotlin/com/meloda/fast/data/account/AccountApi.kt deleted file mode 100644 index bbd182e2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/account/AccountApi.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.data.account - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.account.AccountUrls -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.QueryMap - -interface AccountApi { - - @GET(AccountUrls.SetOnline) - suspend fun setOnline(@QueryMap params: Map): ApiAnswer> - - @POST(AccountUrls.SetOffline) - suspend fun setOffline(@QueryMap params: Map): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/account/AccountsDao.kt b/app/src/main/kotlin/com/meloda/fast/data/account/AccountsDao.kt deleted file mode 100644 index bbd3f79f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/account/AccountsDao.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.data.account - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.model.AppAccount - -@Dao -interface AccountsDao { - - @Query("SELECT * FROM accounts") - suspend fun getAll(): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - - @Query("DELETE FROM accounts WHERE userId = :userId") - suspend fun deleteById(userId: Int) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/account/AccountsRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/account/AccountsRepository.kt deleted file mode 100644 index cf427fab..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/account/AccountsRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.data.account - -import com.meloda.fast.api.network.account.AccountSetOfflineRequest -import com.meloda.fast.api.network.account.AccountSetOnlineRequest - -class AccountsRepository( - private val accountApi: AccountApi, - private val accountsDao: AccountsDao -) { - - suspend fun setOnline(params: AccountSetOnlineRequest) = accountApi.setOnline(params.map) - - suspend fun setOffline(params: AccountSetOfflineRequest) = accountApi.setOffline(params.map) - - - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosApi.kt b/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosApi.kt deleted file mode 100644 index 1a3a33d3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosApi.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.meloda.fast.data.audios - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.attachments.BaseVkAudio -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.audio.AudiosGetUploadServerResponse -import com.meloda.fast.api.network.audio.AudiosUploadResponse -import com.meloda.fast.api.network.audio.AudiosUrls -import okhttp3.MultipartBody -import retrofit2.http.* - -interface AudiosApi { - - @POST(AudiosUrls.GetUploadServer) - suspend fun getUploadServer(): ApiAnswer> - - @Multipart - @POST - suspend fun upload( - @Url url: String, - @Part file: MultipartBody.Part - ): ApiAnswer - - @FormUrlEncoded - @POST(AudiosUrls.Save) - suspend fun save(@FieldMap map: Map): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosRepository.kt deleted file mode 100644 index 5a6a145e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosRepository.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.data.audios - -import okhttp3.MultipartBody - -class AudiosRepository( - private val audiosApi: AudiosApi -) { - - suspend fun getUploadServer() = audiosApi.getUploadServer() - - suspend fun upload(url: String, file: MultipartBody.Part) = audiosApi.upload(url, file) - - suspend fun save(server: Int, audio: String, hash: String) = audiosApi.save( - mapOf( - "server" to server.toString(), - "audio" to audio, - "hash" to hash - ) - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/auth/AuthApi.kt b/app/src/main/kotlin/com/meloda/fast/data/auth/AuthApi.kt deleted file mode 100644 index feb0bf98..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/auth/AuthApi.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.meloda.fast.data.auth - -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.auth.AuthDirectResponse -import com.meloda.fast.api.network.auth.AuthUrls -import com.meloda.fast.api.network.auth.SendSmsResponse -import retrofit2.http.GET -import retrofit2.http.Query -import retrofit2.http.QueryMap - -interface AuthApi { - - @GET(AuthUrls.DirectAuth) - suspend fun auth(@QueryMap param: Map): ApiAnswer - - @GET(AuthUrls.SendSms) - suspend fun sendSms(@Query("sid") validationSid: String): ApiAnswer - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/auth/AuthRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/auth/AuthRepository.kt deleted file mode 100644 index 9042d9e5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/auth/AuthRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.data.auth - -import com.meloda.fast.api.network.auth.AuthDirectRequest - -class AuthRepository( - private val authApi: AuthApi -) { - - suspend fun auth(params: AuthDirectRequest) = authApi.auth(params.map) - - suspend fun sendSms(validationSid: String) = authApi.sendSms(validationSid) - - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsApi.kt b/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsApi.kt deleted file mode 100644 index 02b939e9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsApi.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.data.conversations - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.conversations.ConversationsGetResponse -import com.meloda.fast.api.network.conversations.ConversationsUrls -import retrofit2.http.FieldMap -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface ConversationsApi { - - @FormUrlEncoded - @POST(ConversationsUrls.Get) - suspend fun get(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(ConversationsUrls.Delete) - suspend fun delete(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(ConversationsUrls.Pin) - suspend fun pin(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(ConversationsUrls.Unpin) - suspend fun unpin(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(ConversationsUrls.ReorderPinned) - suspend fun reorderPinned(@FieldMap params: Map): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsDao.kt b/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsDao.kt deleted file mode 100644 index 0804a222..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsDao.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.data.conversations - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.api.model.domain.VkConversationDomain - -@Dao -interface ConversationsDao { - - @Query("SELECT * FROM conversations") - suspend fun getAll(): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsRepository.kt deleted file mode 100644 index 9fd2049e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsRepository.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.meloda.fast.data.conversations - -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest -import com.meloda.fast.api.network.conversations.ConversationsGetRequest -import com.meloda.fast.api.network.conversations.ConversationsPinRequest -import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest - -class ConversationsRepository( - private val conversationsApi: ConversationsApi, - private val conversationsDao: ConversationsDao -) { - - suspend fun get(params: ConversationsGetRequest) = conversationsApi.get(params.map) - - suspend fun delete(params: ConversationsDeleteRequest) = conversationsApi.delete(params.map) - - suspend fun pin(params: ConversationsPinRequest) = conversationsApi.pin(params.map) - - suspend fun unpin(params: ConversationsUnpinRequest) = conversationsApi.unpin(params.map) - - suspend fun store(conversations: List) = conversationsDao.insert(conversations) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/files/FilesApi.kt b/app/src/main/kotlin/com/meloda/fast/data/files/FilesApi.kt deleted file mode 100644 index 73cdad7e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/files/FilesApi.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.data.files - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.files.FilesGetMessagesUploadServerResponse -import com.meloda.fast.api.network.files.FilesSaveFileResponse -import com.meloda.fast.api.network.files.FilesUploadFileResponse -import com.meloda.fast.api.network.files.FilesUrls -import okhttp3.MultipartBody -import retrofit2.http.* - -interface FilesApi { - - @FormUrlEncoded - @POST(FilesUrls.GetMessagesUploadServer) - suspend fun getUploadServer( - @FieldMap map: Map - ): ApiAnswer> - - @Multipart - @POST - suspend fun upload( - @Url url: String, - @Part file: MultipartBody.Part - ): ApiAnswer - - @FormUrlEncoded - @POST(FilesUrls.Save) - suspend fun save( - @FieldMap map: Map - ): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/files/FilesRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/files/FilesRepository.kt deleted file mode 100644 index ec894800..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/files/FilesRepository.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.data.files - -import com.google.gson.annotations.SerializedName -import okhttp3.MultipartBody - -class FilesRepository( - private val filesApi: FilesApi -) { - - enum class FileType(val value: String) { - @SerializedName("doc") - File("doc"), - - @SerializedName("audio_message") - VoiceMessage("audio_message") - } - - suspend fun getMessagesUploadServer(peerId: Int, type: FileType) = - filesApi.getUploadServer( - mapOf( - "peer_id" to peerId.toString(), - "type" to type.value - ) - ) - - suspend fun uploadFile(url: String, file: MultipartBody.Part) = filesApi.upload(url, file) - - suspend fun saveMessageFile(file: String) = filesApi.save(mapOf("file" to file)) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsDao.kt b/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsDao.kt deleted file mode 100644 index 87d7ae6e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.meloda.fast.data.groups - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.api.model.VkGroup - -@Dao -interface GroupsDao { - - @Query("SELECT * FROM groups") - suspend fun getAll(): List - - @Query("SELECT * FROM groups WHERE id = :id") - suspend fun getById(id: Int): VkGroup? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - - suspend fun insert(values: Array) = insert(values.toList()) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsRepository.kt deleted file mode 100644 index 50ba5c0d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsRepository.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.data.groups - -class GroupsRepository( - private val groupsDao: GroupsDao -) { -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/longpoll/LongPollApi.kt b/app/src/main/kotlin/com/meloda/fast/data/longpoll/LongPollApi.kt deleted file mode 100644 index a2ab6688..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/longpoll/LongPollApi.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.data.longpoll - -import com.google.gson.JsonObject -import com.meloda.fast.api.network.ApiAnswer -import retrofit2.http.GET -import retrofit2.http.QueryMap -import retrofit2.http.Url - -interface LongPollApi { - - @GET - suspend fun getResponse( - @Url serverUrl: String, - @QueryMap params: Map - ): ApiAnswer - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesApi.kt b/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesApi.kt deleted file mode 100644 index c12d1fd0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesApi.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.meloda.fast.data.messages - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.BaseVkChat -import com.meloda.fast.api.model.base.BaseVkLongPoll -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.messages.MessagesGetByIdResponse -import com.meloda.fast.api.network.messages.MessagesGetConversationMembersResponse -import com.meloda.fast.api.network.messages.MessagesGetHistoryResponse -import com.meloda.fast.api.network.messages.MessagesUrls -import retrofit2.http.FieldMap -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface MessagesApi { - - @FormUrlEncoded - @POST(MessagesUrls.GetHistory) - suspend fun getHistory(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Send) - suspend fun send(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.MarkAsImportant) - suspend fun markAsImportant(@FieldMap params: Map): ApiAnswer>> - - @FormUrlEncoded - @POST(MessagesUrls.GetLongPollServer) - suspend fun getLongPollServer(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Pin) - suspend fun pin(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Unpin) - suspend fun unpin(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Delete) - suspend fun delete(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Edit) - suspend fun edit(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.GetById) - suspend fun getById(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.MarkAsRead) - suspend fun markAsRead(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.GetChat) - suspend fun getChat(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.GetConversationMembers) - suspend fun getConversationMembers(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.RemoveChatUser) - suspend fun removeChatUser(@FieldMap params: Map): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesDao.kt b/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesDao.kt deleted file mode 100644 index 0953ae5e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesDao.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.data.messages - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.api.model.VkMessage - -@Dao -interface MessagesDao { - - @Query("SELECT * FROM messages") - suspend fun getAll(): List - - @Query("SELECT * FROM messages WHERE id = :id") - suspend fun getById(id: Int): VkMessage? - - @Query("SELECT * FROM messages WHERE peerId = :peerId") - suspend fun getByPeerId(peerId: Int): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - - suspend fun insert(values: Array) = insert(values.toList()) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesRepository.kt deleted file mode 100644 index f1c70b60..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesRepository.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.meloda.fast.data.messages - -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest -import com.meloda.fast.api.network.messages.MessagesDeleteRequest -import com.meloda.fast.api.network.messages.MessagesEditRequest -import com.meloda.fast.api.network.messages.MessagesGetByIdRequest -import com.meloda.fast.api.network.messages.MessagesGetChatRequest -import com.meloda.fast.api.network.messages.MessagesGetConversationMembersRequest -import com.meloda.fast.api.network.messages.MessagesGetHistoryRequest -import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest -import com.meloda.fast.api.network.messages.MessagesMarkAsImportantRequest -import com.meloda.fast.api.network.messages.MessagesPinMessageRequest -import com.meloda.fast.api.network.messages.MessagesRemoveChatUserRequest -import com.meloda.fast.api.network.messages.MessagesSendRequest -import com.meloda.fast.api.network.messages.MessagesUnPinMessageRequest -import com.meloda.fast.data.longpoll.LongPollApi - -class MessagesRepository( - private val messagesApi: MessagesApi, - private val messagesDao: MessagesDao, - private val longPollApi: LongPollApi, -) { - - suspend fun store(message: VkMessage) = store(listOf(message)) - - suspend fun store(messages: List) = messagesDao.insert(messages) - - suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId) - - suspend fun getHistory(params: MessagesGetHistoryRequest) = - messagesApi.getHistory(params.map) - - suspend fun send(params: MessagesSendRequest) = - messagesApi.send(params.map) - - suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) = - messagesApi.markAsImportant(params.map) - - suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = - messagesApi.getLongPollServer(params.map) - - suspend fun pin(params: MessagesPinMessageRequest) = - messagesApi.pin(params.map) - - suspend fun unpin(params: MessagesUnPinMessageRequest) = - messagesApi.unpin(params.map) - - suspend fun delete(params: MessagesDeleteRequest) = - messagesApi.delete(params.map) - - suspend fun edit(params: MessagesEditRequest) = - messagesApi.edit(params.map) - - suspend fun getLongPollUpdates( - serverUrl: String, - params: LongPollGetUpdatesRequest, - ) = longPollApi.getResponse(serverUrl, params.map) - - suspend fun getById(params: MessagesGetByIdRequest) = - messagesApi.getById(params.map) - - suspend fun markAsRead( - peerId: Int, - messagesIds: List? = null, - startMessageId: Int? = null, - ) = messagesApi.markAsRead( - mutableMapOf("peer_id" to peerId.toString()).apply { - messagesIds?.let { - this["message_ids"] = messagesIds.joinToString { it.toString() } - } - startMessageId?.let { - this["start_message_id"] = it.toString() - } - } - ) - - suspend fun getChat( - chatId: Int, - fields: String? = null, - ) = messagesApi.getChat(MessagesGetChatRequest(chatId, fields).map) - - suspend fun getConversationMembers( - peerId: Int, - offset: Int? = null, - count: Int? = null, - extended: Boolean? = null, - fields: String? = null, - ) = messagesApi.getConversationMembers( - MessagesGetConversationMembersRequest( - peerId, - offset, - count, - extended, - fields - ).map - ) - - suspend fun removeChatUser( - chatId: Int, - memberId: Int, - ) = messagesApi.removeChatUser(MessagesRemoveChatUserRequest(chatId, memberId).map) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/ota/OtaApi.kt b/app/src/main/kotlin/com/meloda/fast/data/ota/OtaApi.kt deleted file mode 100644 index 68ea9737..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/ota/OtaApi.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.data.ota - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse -import com.meloda.fast.api.network.ota.OtaUrls -import com.meloda.fast.model.UpdateActualUrl -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.Query -import retrofit2.http.Url - -interface OtaApi { - - @GET(OtaUrls.GetActualUrl) - suspend fun getActualUrl(): ApiAnswer - - @GET - suspend fun getLatestRelease( - @Url url: String, - @Query("productId") productId: Int = 28, - @Query("branchId") branchId: Int = 10, - @Header("Secret-Code") secretCode: String - ): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosApi.kt b/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosApi.kt deleted file mode 100644 index 863388e0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosApi.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.data.photos - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.attachments.BaseVkPhoto -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.photos.PhotoUrls -import com.meloda.fast.api.network.photos.PhotosGetMessagesUploadServerResponse -import com.meloda.fast.api.network.photos.PhotosUploadPhotoResponse -import okhttp3.MultipartBody -import retrofit2.http.* - -interface PhotosApi { - - @FormUrlEncoded - @POST(PhotoUrls.GetMessagesUploadServer) - suspend fun getUploadServer( - @FieldMap map: Map - ): ApiAnswer> - - @Multipart - @POST - suspend fun upload( - @Url url: String, - @Part photo: MultipartBody.Part - ): ApiAnswer - - @FormUrlEncoded - @POST(PhotoUrls.SaveMessagePhoto) - suspend fun save( - @FieldMap map: Map - ): ApiAnswer>> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosRepository.kt deleted file mode 100644 index e143c7a6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.data.photos - -import com.meloda.fast.api.network.photos.PhotosSaveMessagePhotoRequest -import okhttp3.MultipartBody - -class PhotosRepository( - private val photosApi: PhotosApi -) { - - suspend fun getMessagesUploadServer(peerId: Int) = - photosApi.getUploadServer(mapOf("peer_id" to peerId.toString())) - - suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = photosApi.upload(url, photo) - - suspend fun saveMessagePhoto(body: PhotosSaveMessagePhotoRequest) = - photosApi.save(body.map) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/users/UsersApi.kt b/app/src/main/kotlin/com/meloda/fast/data/users/UsersApi.kt deleted file mode 100644 index 7574536a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/users/UsersApi.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.meloda.fast.data.users - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.BaseVkUser -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.users.UsersUrls -import retrofit2.http.FieldMap -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface UsersApi { - - @FormUrlEncoded - @POST(UsersUrls.GetById) - suspend fun getById( - @FieldMap params: Map? - ): ApiAnswer>> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/users/UsersDao.kt b/app/src/main/kotlin/com/meloda/fast/data/users/UsersDao.kt deleted file mode 100644 index 3d74af1d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/users/UsersDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.meloda.fast.data.users - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.api.model.VkUser - -@Dao -interface UsersDao { - - @Query("SELECT * FROM users") - suspend fun getAll(): List - - @Query("SELECT * FROM users WHERE id = :id") - suspend fun getById(id: Int): VkUser? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - - suspend fun insert(values: Array) = insert(values.toList()) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/users/UsersRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/users/UsersRepository.kt deleted file mode 100644 index f41f0b17..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/users/UsersRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.data.users - -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.network.users.UsersGetRequest - -class UsersRepository( - private val usersApi: UsersApi, - private val usersDao: UsersDao -) { - - suspend fun getById(params: UsersGetRequest) = usersApi.getById(params.map) - - suspend fun storeUsers(users: List) { - usersDao.insert(users) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/videos/VideosApi.kt b/app/src/main/kotlin/com/meloda/fast/data/videos/VideosApi.kt deleted file mode 100644 index 1b244480..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/videos/VideosApi.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.data.videos - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.videos.VideosSaveResponse -import com.meloda.fast.api.network.videos.VideosUploadResponse -import com.meloda.fast.api.network.videos.VideosUrls -import okhttp3.MultipartBody -import retrofit2.http.Multipart -import retrofit2.http.POST -import retrofit2.http.Part -import retrofit2.http.Url - -interface VideosApi { - - @POST(VideosUrls.Save) - suspend fun save(): ApiAnswer> - - @Multipart - @POST - suspend fun upload( - @Url url: String, - @Part file: MultipartBody.Part - ): ApiAnswer - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/videos/VideosRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/videos/VideosRepository.kt deleted file mode 100644 index 0b9f2866..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/videos/VideosRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.data.videos - -import okhttp3.MultipartBody - -class VideosRepository( - private val videosApi: VideosApi -) { - - suspend fun save() = videosApi.save() - - suspend fun upload(url: String, file: MultipartBody.Part) = videosApi.upload(url, file) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/database/AccountsDatabase.kt b/app/src/main/kotlin/com/meloda/fast/database/AccountsDatabase.kt deleted file mode 100644 index a5ec4ef4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/database/AccountsDatabase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.database - -import androidx.room.Database -import androidx.room.RoomDatabase -import com.meloda.fast.data.account.AccountsDao -import com.meloda.fast.model.AppAccount - -@Database( - entities = [AppAccount::class], - version = 1, - exportSchema = false -) -abstract class AccountsDatabase : RoomDatabase() { - abstract val accountsDao: AccountsDao -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/database/CacheDatabase.kt b/app/src/main/kotlin/com/meloda/fast/database/CacheDatabase.kt deleted file mode 100644 index fa8df9ea..00000000 --- a/app/src/main/kotlin/com/meloda/fast/database/CacheDatabase.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.database - -import androidx.room.Database -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -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.domain.VkConversationDomain -import com.meloda.fast.data.conversations.ConversationsDao -import com.meloda.fast.data.groups.GroupsDao -import com.meloda.fast.data.messages.MessagesDao -import com.meloda.fast.data.users.UsersDao - -@Database( - entities = [ - VkConversationDomain::class, - VkMessage::class, - VkUser::class, - VkGroup::class - ], - version = 42, - exportSchema = false -) -@TypeConverters(Converters::class) -abstract class CacheDatabase : RoomDatabase() { - - abstract val conversationsDao: ConversationsDao - abstract val messagesDao: MessagesDao - abstract val usersDao: UsersDao - abstract val groupsDao: GroupsDao - -} diff --git a/app/src/main/kotlin/com/meloda/fast/database/Converters.kt b/app/src/main/kotlin/com/meloda/fast/database/Converters.kt deleted file mode 100644 index 5b852294..00000000 --- a/app/src/main/kotlin/com/meloda/fast/database/Converters.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.meloda.fast.database - -import androidx.room.TypeConverter -import com.google.gson.Gson -import com.meloda.fast.api.base.AttachmentClassNameIsEmptyException -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.attachments.VkAttachment -import com.meloda.fast.api.model.base.BaseVkMessage -import org.json.JSONObject - -@Suppress("UnnecessaryVariable") -class Converters { - - private companion object { - private const val CACHE_SEPARATOR = "fastkruta228355" - } - - @TypeConverter - fun fromGeoToString(geo: BaseVkMessage.Geo?): String? { - if (geo == null) return null - - return try { - val string = Gson().toJson(geo) - - return string - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromStringToGeo(string: String?): BaseVkMessage.Geo? { - if (string == null) return null - - return try { - val geo = Gson().fromJson(string, BaseVkMessage.Geo::class.java) - - return geo - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromListVkMessageToString(messages: List?): String? { - if (messages == null) return null - - val string = messages - .mapNotNull(::fromVkMessageToString) - .joinToString(separator = CACHE_SEPARATOR) - - return string - } - - @TypeConverter - fun fromStringToListVkMessage(string: String?): List? { - if (string == null) return null - - if (string.contains(CACHE_SEPARATOR)) { - val messages = string - .split(CACHE_SEPARATOR) - .mapNotNull(::fromStringToVkMessage) - return messages - } - - - val message = fromStringToVkMessage(string) - return message?.let { listOf(it) } - } - - @TypeConverter - fun fromVkMessageToString(message: VkMessage?): String? { - if (message == null) return null - - return try { - val string = Gson().toJson(message) - - return string - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromStringToVkMessage(string: String?): VkMessage? { - if (string == null) return null - - return try { - val message = Gson().fromJson(string, VkMessage::class.java) - - return message - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromListVkAttachmentToString(attachments: List?): String? { - if (attachments == null) return null - - val string = attachments - .mapNotNull(::fromVkAttachmentToString) - .joinToString(separator = CACHE_SEPARATOR) - return string - } - - @TypeConverter - fun fromStringToListVkAttachment(string: String?): List? { - if (string == null) return null - - if (string.contains(CACHE_SEPARATOR)) { - val attachments = string - .split(CACHE_SEPARATOR) - .mapNotNull(::fromStringToVkAttachment) - return attachments - } - - val attachment = fromStringToVkAttachment(string) - - return attachment?.let { listOf(it) } - } - - @TypeConverter - fun fromVkAttachmentToString(attachment: VkAttachment?): String? { - if (attachment == null) return null - - try { - attachment.javaClass.getDeclaredField("className") - } catch (e: NoSuchFieldException) { - throw AttachmentClassNameIsEmptyException(attachment) - } - return try { - val string = Gson().toJson(attachment) - string - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromStringToVkAttachment(string: String?): VkAttachment? { - if (string.isNullOrBlank()) return null - - return try { - val className = JSONObject(string).optString("className") - - val attachment = Gson().fromJson(string, Class.forName(className)) as? VkAttachment? - - return attachment - } catch (e: Exception) { - e.printStackTrace() - null - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/di/ApiModule.kt b/app/src/main/kotlin/com/meloda/fast/di/ApiModule.kt deleted file mode 100644 index 275c38b5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/ApiModule.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.meloda.fast.di - -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.data.account.AccountApi -import com.meloda.fast.data.audios.AudiosApi -import com.meloda.fast.data.auth.AuthApi -import com.meloda.fast.data.conversations.ConversationsApi -import com.meloda.fast.data.files.FilesApi -import com.meloda.fast.data.longpoll.LongPollApi -import com.meloda.fast.data.messages.MessagesApi -import com.meloda.fast.data.ota.OtaApi -import com.meloda.fast.data.photos.PhotosApi -import com.meloda.fast.data.users.UsersApi -import com.meloda.fast.data.videos.VideosApi -import org.koin.core.module.dsl.singleOf -import org.koin.core.scope.Scope -import org.koin.dsl.module - -val apiModule = module { - single { api(AuthApi::class.java) } - single { api(ConversationsApi::class.java) } - single { api(UsersApi::class.java) } - single { api(MessagesApi::class.java) } - single { api(LongPollApi::class.java) } - single { api(AccountApi::class.java) } - single { api(OtaApi::class.java) } - single { api(PhotosApi::class.java) } - single { api(VideosApi::class.java) } - single { api(AudiosApi::class.java) } - single { api(FilesApi::class.java) } - - singleOf(::LongPollUpdatesParser) -} - -internal fun Scope.api(className: Class): T = retrofit().create(className) diff --git a/app/src/main/kotlin/com/meloda/fast/di/DataModule.kt b/app/src/main/kotlin/com/meloda/fast/di/DataModule.kt deleted file mode 100644 index 9397fd8a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/DataModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.di - -import com.meloda.fast.data.account.AccountsRepository -import com.meloda.fast.data.audios.AudiosRepository -import com.meloda.fast.data.auth.AuthRepository -import com.meloda.fast.data.conversations.ConversationsRepository -import com.meloda.fast.data.files.FilesRepository -import com.meloda.fast.data.messages.MessagesRepository -import com.meloda.fast.data.photos.PhotosRepository -import com.meloda.fast.data.users.UsersRepository -import com.meloda.fast.data.videos.VideosRepository -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module - -// TODO: 17.04.2023, Danil Nikolaev: use specific repositories in local DI modules -val dataModule = module { - singleOf(::ConversationsRepository) - singleOf(::MessagesRepository) - singleOf(::UsersRepository) - singleOf(::AuthRepository) - singleOf(::AccountsRepository) - singleOf(::PhotosRepository) - singleOf(::VideosRepository) - singleOf(::AudiosRepository) - singleOf(::FilesRepository) -} diff --git a/app/src/main/kotlin/com/meloda/fast/di/DatabaseModule.kt b/app/src/main/kotlin/com/meloda/fast/di/DatabaseModule.kt deleted file mode 100644 index bd5615ce..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/DatabaseModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.meloda.fast.di - -import androidx.room.Room -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.database.AccountsDatabase -import com.meloda.fast.database.CacheDatabase -import org.koin.core.scope.Scope -import org.koin.dsl.module - -val databaseModule = module { - single { - Room.databaseBuilder(AppGlobal.Instance, CacheDatabase::class.java, "cache") - .fallbackToDestructiveMigration() - .build() - } - single { - Room.databaseBuilder(AppGlobal.Instance, AccountsDatabase::class.java, "accounts") - .build() - } - single { cache().conversationsDao } - single { cache().messagesDao } - single { cache().usersDao } - single { cache().groupsDao } - single { accounts().accountsDao } -} - -private fun Scope.cache(): CacheDatabase = get() -private fun Scope.accounts(): AccountsDatabase = get() diff --git a/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt b/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt deleted file mode 100644 index 3b130ede..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.meloda.fast.di - -import com.github.terrakok.cicerone.Cicerone -import com.github.terrakok.cicerone.Router -import com.meloda.fast.screens.captcha.screen.CaptchaScreen -import com.meloda.fast.screens.twofa.screen.TwoFaScreen -import org.koin.core.module.dsl.singleOf -import org.koin.core.scope.Scope -import org.koin.dsl.module - -val navigationModule = module { - single { Cicerone.create() } - single { cicerone().router } - single { cicerone().getNavigatorHolder() } - - singleOf(::CaptchaScreen) - singleOf(::TwoFaScreen) -} - -private fun Scope.cicerone(): Cicerone = get() diff --git a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt deleted file mode 100644 index b7a546ab..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.meloda.fast.di - -import com.chuckerteam.chucker.api.ChuckerCollector -import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.google.gson.GsonBuilder -import com.meloda.fast.api.network.AuthInterceptor -import com.meloda.fast.api.network.ResultCallFactory -import com.meloda.fast.api.network.VkUrls -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import org.koin.core.module.dsl.singleOf -import org.koin.core.scope.Scope -import org.koin.dsl.module -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.util.concurrent.TimeUnit - -val networkModule = module { - single { ChuckerCollector(get()) } - single { ChuckerInterceptor.Builder(get()).collector(get()).build() } - singleOf(::AuthInterceptor) - single { GsonBuilder().setLenient().create() } - single { - OkHttpClient.Builder() - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .addInterceptor(authInterceptor()) - .addInterceptor( - chuckerInterceptor().apply { - redactHeader("Secret-Code") - } - ) - .followRedirects(true) - .followSslRedirects(true) - .addInterceptor( - HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - ).build() - } - single { - Retrofit.Builder() - .baseUrl("${VkUrls.API}/") - .addConverterFactory(GsonConverterFactory.create(get())) - .addCallAdapterFactory(ResultCallFactory(get())) - .client(get()) - .build() - } -} - -internal fun Scope.retrofit(): Retrofit = get() -private fun Scope.authInterceptor(): AuthInterceptor = get() -private fun Scope.chuckerInterceptor(): ChuckerInterceptor = get() diff --git a/app/src/main/kotlin/com/meloda/fast/di/OtaModule.kt b/app/src/main/kotlin/com/meloda/fast/di/OtaModule.kt deleted file mode 100644 index 413a16b3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/OtaModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.di - -import com.meloda.fast.common.UpdateManager -import com.meloda.fast.common.UpdateManagerImpl -import com.meloda.fast.data.ota.OtaApi -import org.koin.core.module.dsl.bind -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module - -val otaModule = module { - single { api(OtaApi::class.java) } - singleOf(::UpdateManagerImpl) { bind() } -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/ActivityExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/ActivityExt.kt deleted file mode 100644 index f75afa5a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/ActivityExt.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.ext - -import android.app.Activity -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.flow.Flow - -fun Activity.edgeToEdge() { - WindowCompat.setDecorFitsSystemWindows(window, false) -} - -context(AppCompatActivity) -fun Flow.listenValue(action: suspend (T) -> Unit) = listenValue(lifecycleScope, action) diff --git a/app/src/main/kotlin/com/meloda/fast/ext/AndroidVersionsExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/AndroidVersionsExt.kt deleted file mode 100644 index c65f885c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/AndroidVersionsExt.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.meloda.fast.ext - -import android.os.Build - -fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean { - return if (Build.VERSION.SDK_INT >= sdkInt) { - action?.invoke() - true - } else { - false - } -} - -fun sdkAndUp(sdkInt: Int, action: () -> Unit): Boolean? { - return if (Build.VERSION.SDK_INT >= sdkInt) { - action.invoke() - true - } else null -} - -fun isSdkAtLeastOr( - sdkInt: Int, - action: (() -> Unit)? = null, - orAction: (() -> Unit)? = null -): Boolean { - return if (Build.VERSION.SDK_INT >= sdkInt) { - action?.invoke() - true - } else { - orAction?.invoke() - false - } -} - -fun sdk26AndUp(action: () -> Unit): Boolean? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - action.invoke() - true - } else null -} - -fun sdk30AndUp(action: () -> Unit): Boolean? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - action.invoke() - true - } else null -} - -fun sdk33AndUp(action: () -> Unit): Boolean? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - action.invoke() - true - } else null -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/BooleanExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/BooleanExt.kt deleted file mode 100644 index a1007d57..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/BooleanExt.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.meloda.fast.ext - -val Boolean?.isTrue: Boolean get() = this == true - -val Boolean?.isFalse: Boolean get() = this == false diff --git a/app/src/main/kotlin/com/meloda/fast/ext/BundleExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/BundleExt.kt deleted file mode 100644 index bc31d952..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/BundleExt.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.meloda.fast.ext - -import android.os.Build -import android.os.Bundle -import android.os.Parcelable -import java.io.Serializable - -@Suppress("UNCHECKED_CAST", "DEPRECATION") -fun Bundle.getParcelableArrayListCompat( - key: String?, - clazz: Class -): java.util.ArrayList? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelableArrayList(key, clazz) - } else { - getParcelableArrayList(key) as ArrayList - } -} - -@Suppress("DEPRECATION") -fun Bundle.getParcelableCompat(key: String?, clazz: Class): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, clazz) - } else { - getParcelable(key) - } -} - -@Suppress("DEPRECATION", "UNCHECKED_CAST") -fun Bundle.getSerializableCompat(key: String?, clazz: Class): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getSerializable(key, clazz) - } else { - getSerializable(key) as? T - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/ext/ComposeExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/ComposeExt.kt deleted file mode 100644 index 8ab3fa82..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/ComposeExt.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.meloda.fast.ext - -import android.content.res.Configuration -import android.media.AudioManager -import android.view.KeyEvent -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Indication -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.semantics.Role -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.AndroidUtils - -@ExperimentalFoundationApi -fun Modifier.clickableSound( - enabled: Boolean = true, - onClickLabel: String? = null, - role: Role? = null, - onClick: (() -> Unit)? = null -): Modifier = this.clickable( - enabled = enabled, - onClickLabel = onClickLabel, - role = role, - onClick = { - AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK) - onClick?.invoke() - } -) - -@ExperimentalFoundationApi -fun Modifier.combinedClickableSound( - enabled: Boolean = true, - onClickLabel: String? = null, - role: Role? = null, - onLongClickLabel: String? = null, - onLongClick: (() -> Unit)? = null, - onDoubleClick: (() -> Unit)? = null, - onClick: (() -> Unit)? = null -): Modifier = composed { - this.combinedClickableSound( - interactionSource = remember { MutableInteractionSource() }, - indication = LocalIndication.current, - enabled = enabled, - onClickLabel = onClickLabel, - role = role, - onLongClickLabel = onLongClickLabel, - onLongClick = onLongClick, - onDoubleClick = onDoubleClick, - onClick = { - AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK) - onClick?.invoke() - } - ) -} - -@ExperimentalFoundationApi -fun Modifier.combinedClickableSound( - interactionSource: MutableInteractionSource, - indication: Indication?, - enabled: Boolean = true, - onClickLabel: String? = null, - role: Role? = null, - onLongClickLabel: String? = null, - onLongClick: (() -> Unit)? = null, - onDoubleClick: (() -> Unit)? = null, - onClick: (() -> Unit)? = null -): Modifier = this.combinedClickable( - interactionSource = interactionSource, - indication = indication, - enabled = enabled, - onClickLabel = onClickLabel, - role = role, - onLongClickLabel = onLongClickLabel, - onLongClick = onLongClick, - onDoubleClick = onDoubleClick, - onClick = { - AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK) - onClick?.invoke() - } -) - -fun Modifier.handleTabKey( - action: () -> Boolean -): Modifier = this.onKeyEvent { event -> - if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB) { - action.invoke() - } else false -} - -fun Modifier.handleEnterKey( - action: () -> Boolean -): Modifier = this.onKeyEvent { event -> - if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { - action.invoke() - } else false -} - -@Composable -fun UiText?.getString(): String? { - return this.parseString(LocalContext.current) -} - -@Composable -fun isUsingDarkTheme(): Boolean { - if (LocalView.current.isInEditMode) { - return false - } - - val nightThemeMode = AppGlobal.preferences.getInt( - SettingsFragment.KEY_APPEARANCE_DARK_THEME, - SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME - ) - val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES - val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY - - val systemUiNightMode = AppGlobal.resources.configuration.uiMode - - val isSystemBatterySaver = AndroidUtils.isBatterySaverOn() - val isSystemUsingDarkTheme = - systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES - - return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) -} - -@Composable -fun isUsingDynamicColors(): Boolean = - if (LocalView.current.isInEditMode) true - else { - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_DYNAMIC_COLORS, - SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - } diff --git a/app/src/main/kotlin/com/meloda/fast/ext/ContextExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/ContextExt.kt deleted file mode 100644 index a7fd3e66..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/ContextExt.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.meloda.fast.ext - -import android.content.Context -import android.view.View -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString - -fun Context.showDialog( - title: UiText? = null, - message: UiText? = null, - isCancelable: Boolean = true, - positiveText: UiText? = null, - positiveAction: (() -> Unit)? = null, - negativeText: UiText? = null, - negativeAction: (() -> Unit)? = null, - neutralText: UiText? = null, - neutralAction: (() -> Unit)? = null, - onDismissAction: (() -> Unit)? = null, - view: View? = null, - items: List? = null, - itemsChoiceType: ItemsChoiceType = ItemsChoiceType.None, - itemsClickAction: ((index: Int, value: String) -> Unit)? = null, - itemsMultiChoiceClickAction: ((index: Int, value: String, isChecked: Boolean) -> Unit)? = null, - checkedItems: List? = null -): AlertDialog { - val builder = MaterialAlertDialogBuilder(this) - .setCancelable(isCancelable) - .setOnDismissListener { onDismissAction?.invoke() } - - title?.asString()?.let(builder::setTitle) - message?.asString()?.let(builder::setMessage) - - view?.let(builder::setView) - - positiveText?.let { text -> - builder.setPositiveButton(text.asString()) { _, _ -> positiveAction?.invoke() } - } - negativeText?.let { text -> - builder.setNegativeButton(text.asString()) { _, _ -> negativeAction?.invoke() } - } - neutralText?.let { text -> - builder.setNeutralButton(text.asString()) { _, _ -> neutralAction?.invoke() } - } - - items?.mapNotNull { it.asString() }?.let { stringItems -> - when (itemsChoiceType) { - ItemsChoiceType.None -> { - builder.setItems( - stringItems.toTypedArray() - ) { dialog, which -> - dialog.dismiss() - itemsClickAction?.invoke(which, stringItems[which]) - } - } - - ItemsChoiceType.SingleChoice -> { - builder.setSingleChoiceItems( - stringItems.toTypedArray(), - checkedItems?.first() ?: -1 - ) { _, which -> - itemsClickAction?.invoke(which, stringItems[which]) - } - } - - ItemsChoiceType.MultiChoice -> { - builder.setMultiChoiceItems( - stringItems.toTypedArray(), - BooleanArray(stringItems.size) { index -> checkedItems?.contains(index).isTrue } - ) { _, which, isChecked -> - itemsMultiChoiceClickAction?.invoke(which, stringItems[which], isChecked) - } - } - } - } - - return builder.show() -} - -sealed class ItemsChoiceType { - object None : ItemsChoiceType() - object SingleChoice : ItemsChoiceType() - object MultiChoice : ItemsChoiceType() -} - -context(Context) -fun UiText?.asString(): String? { - return this.parseString(this@Context) -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/Ext.kt b/app/src/main/kotlin/com/meloda/fast/ext/Ext.kt deleted file mode 100644 index 160e5890..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/Ext.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.meloda.fast.ext - -import android.content.res.Configuration -import android.content.res.Resources -import android.util.DisplayMetrics -import androidx.appcompat.app.AppCompatDelegate -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.common.net.MediaType -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.AndroidUtils -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -@Deprecated("use resources or rewrite in Compose") -fun Int.dpToPx(): Int { - val metrics = Resources.getSystem().displayMetrics - return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() -} - -@Deprecated("use resources or rewrite in Compose") -fun Float.dpToPx(): Int { - val metrics = Resources.getSystem().displayMetrics - return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() -} - -val MediaType.mimeType: String get() = "${type()}/${subtype()}" - -@Throws(NullPointerException::class) -fun T?.notNull(lazyMessage: (() -> Any)? = null): T { - return if (lazyMessage != null) { - requireNotNull(this, lazyMessage) - } else { - requireNotNull(this) - } -} - -inline fun Iterable.findIndex(predicate: (T) -> Boolean): Int? { - return indexOf(firstOrNull(predicate)).let { if (it == -1) null else it } -} - -inline fun > Iterable.toMap( - destination: M, - keySelector: (T) -> K, -): M { - for (element in this) { - val key = keySelector(element) - destination[key] = element - } - return destination -} - -fun MutableList.addIf(element: T, condition: () -> Boolean) { - if (condition.invoke()) add(element) -} - -context(ViewModel) -fun Flow.listenValue(action: suspend (T) -> Unit) = listenValue(viewModelScope, action) - -fun Flow.listenValue( - coroutineScope: CoroutineScope, - action: suspend (T) -> Unit -): Job = onEach(action::invoke).launchIn(coroutineScope) - -fun isSystemUsingDarkMode(): Boolean { - val nightThemeMode = AppGlobal.preferences.getInt( - SettingsFragment.KEY_APPEARANCE_DARK_THEME, - SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME - ) - val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES - val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY - - val systemUiNightMode = AppGlobal.resources.configuration.uiMode - - val isSystemBatterySaver = AndroidUtils.isBatterySaverOn() - val isSystemUsingDarkTheme = - systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES - - return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) -} - -fun createTimerFlow( - time: Int, - onStartAction: suspend () -> Unit, - onTickAction: suspend (remainedTime: Int) -> Unit, - onTimeoutAction: suspend () -> Unit, - interval: Duration = 1.seconds -): Flow = (time downTo 0) - .asSequence() - .asFlow() - .onStart { onStartAction() } - .onEach { timeLeft -> - onTickAction(timeLeft) - if (timeLeft == 0) { - onTimeoutAction() - } else { - delay(interval) - } - } - -fun createTimerFlow( - isNeedToEndCondition: suspend () -> Boolean, - onStartAction: (suspend () -> Unit)? = null, - onTickAction: (suspend () -> Unit)? = null, - onEndAction: (suspend () -> Unit)? = null, - interval: Duration = 1.seconds -): Flow = flow { - while (true) { - val isNeedToEnd = isNeedToEndCondition() - emit(isNeedToEnd) - if (isNeedToEnd) break - } -} - .onStart { onStartAction?.invoke() } - .onEach { isNeedToEnd -> - onTickAction?.invoke() - if (isNeedToEnd) { - onEndAction?.invoke() - } else { - delay(interval) - } - } - -context(ViewModel) -fun MutableSharedFlow.emitOnMainScope(value: T) = emitOnScope(value, Dispatchers.Main) - -context(ViewModel) -fun MutableSharedFlow.emitOnScope( - value: T, - dispatcher: CoroutineDispatcher = Dispatchers.Default, -) { - viewModelScope.launch(dispatcher) { - emit(value) - } -} - -context(CoroutineScope) -suspend fun MutableSharedFlow.emitWithMain(value: T) { - withContext(Dispatchers.Main) { - emit(value) - } -} - -context(ViewModel) -fun MutableStateFlow.updateValue(newValue: T) = this.update { newValue } diff --git a/app/src/main/kotlin/com/meloda/fast/ext/FragmentExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/FragmentExt.kt deleted file mode 100644 index 9663cd05..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/FragmentExt.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.meloda.fast.ext - -import android.graphics.drawable.Drawable -import android.widget.Toast -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString -import kotlinx.coroutines.flow.Flow - -context(Fragment) -fun Flow.listenValue( - action: suspend (T) -> Unit -) = listenValue(lifecycleScope, action) - - -context(Fragment) -fun String.toast(duration: Int = Toast.LENGTH_LONG) = toast(requireContext(), duration) - -context(Fragment) -fun color(@ColorRes resId: Int): Int { - return ContextCompat.getColor(requireContext(), resId) -} - -context(Fragment) -fun drawable(@DrawableRes resId: Int): Drawable? { - return ContextCompat.getDrawable(requireContext(), resId) -} - -context(Fragment) -fun string(@StringRes resId: Int): String { - return getString(resId) -} - -context(Fragment) -fun string(@StringRes resId: Int, vararg args: Any?): String { - return getString(resId, *args) -} - -context(Fragment) -fun UiText?.asString(): String? { - return this.parseString(this@Fragment.requireContext()) -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/GlideExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/GlideExt.kt deleted file mode 100644 index 6f8c2e73..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/GlideExt.kt +++ /dev/null @@ -1,189 +0,0 @@ -package com.meloda.fast.ext - -import android.graphics.Bitmap -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.net.Uri -import android.widget.ImageView -import com.bumptech.glide.Glide -import com.bumptech.glide.Priority -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.Transformation -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.load.resource.bitmap.* -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.RequestOptions -import com.bumptech.glide.request.target.Target - -object ImageLoader { - - val userAvatarTransformations = listOf( - TypeTransformations.CircleCrop - ) - - fun ImageView.clear() { - this.setImageDrawable(null) - } - - fun ImageView.loadWithGlide(block: GlideParams.() -> Unit) { - val params = GlideParams() - block.invoke(params) - loadWithGlide(params) - } - - fun ImageView.loadWithGlide(params: GlideParams) { - val request = Glide.with(this) - - var builder = when { - params.imageUrl != null -> request.load(params.imageUrl) - params.imageUri != null -> request.load(params.imageUri) - params.drawableRes != null -> request.load(params.drawableRes) - drawable != null -> request.load(drawable) - else -> request.load(null as Drawable?) - } - - val transforms = params.transformations.toMutableList() - if (params.asCircle) { - transforms += TypeTransformations.CircleCrop - } - - builder = builder - .apply(TypeTransformations.createRequestOptions(transforms)) - .error( - params.errorDrawable - ?: if (params.errorColor != null) { - ColorDrawable(requireNotNull(params.errorColor)) - } else null - ) - .placeholder( - params.placeholderDrawable - ?: if (params.placeholderColor != null) { - ColorDrawable(requireNotNull(params.placeholderColor)) - } else null - ) - .addListener(ImageLoadRequestListener(params.onLoadedAction, params.onFailedAction)) - .addListener(ImageLoadDoneListener(params.onDoneAction)) - .diskCacheStrategy(params.cacheStrategy) - .priority(params.loadPriority) - - if (params.crossFade || params.crossFadeDuration != null) { - builder = builder.transition(withCrossFade(params.crossFadeDuration ?: 200)) - } - - builder.into(this) - } -} - -data class GlideParams( - var imageUrl: String? = null, - var imageUri: Uri? = null, - var drawableRes: Int? = null, - var imageDrawable: Drawable? = null, - var placeholderDrawable: Drawable? = null, - var placeholderColor: Int? = null, - var errorDrawable: Drawable? = placeholderDrawable, - var errorColor: Int? = null, - var crossFade: Boolean = false, - var crossFadeDuration: Int? = null, - var asCircle: Boolean = false, - var transformations: List = emptyList(), - var onLoadedAction: (() -> Unit)? = null, - var onFailedAction: (() -> Unit)? = null, - var onDoneAction: (() -> Unit)? = null, - var loadPriority: Priority = Priority.NORMAL, - var cacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL, -) - -class ImageLoadRequestListener( - private val onLoadedAction: (() -> Unit)?, - private val onFailedAction: (() -> Unit)?, -) : RequestListener { - - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean, - ): Boolean { - onFailedAction?.invoke() - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean, - ): Boolean { - onLoadedAction?.invoke() - return false - } -} - -class ImageLoadDoneListener(private val onDoneAction: (() -> Unit)?) : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean, - ): Boolean { - onDoneAction?.invoke() - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean, - ): Boolean { - onDoneAction?.invoke() - return false - } -} - -sealed class TypeTransformations { - - object CenterCrop : TypeTransformations() - - object CenterInside : TypeTransformations() - - object CircleCrop : TypeTransformations() - - class RoundedCornerCrop(val radius: Int) : TypeTransformations() - - class GranularRoundedCornerCrop( - val topLeft: Float, - val topRight: Float, - val bottomRight: Float, - val bottomLeft: Float, - ) : TypeTransformations() - - fun toGlideTransform(): Transformation = when (this) { - CenterCrop -> CenterCrop() - CenterInside -> CenterInside() - is RoundedCornerCrop -> RoundedCorners(radius) - is GranularRoundedCornerCrop -> GranularRoundedCorners( - topLeft, - topRight, - bottomRight, - bottomLeft - ) - CircleCrop -> CircleCrop() - } - - companion object { - - fun createRequestOptions(transformations: List): RequestOptions { - val mappedTransformations = transformations - .map(TypeTransformations::toGlideTransform) - .toTypedArray() - - return RequestOptions().transform(* mappedTransformations) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/NumbersExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/NumbersExt.kt deleted file mode 100644 index e8b16cb1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/NumbersExt.kt +++ /dev/null @@ -1 +0,0 @@ -package com.meloda.fast.ext diff --git a/app/src/main/kotlin/com/meloda/fast/ext/StringExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/StringExt.kt deleted file mode 100644 index 3c615a2f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/StringExt.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.ext - -import android.content.Context -import android.widget.Toast -import com.meloda.fast.model.base.UiText - -inline fun String?.ifEmpty(defaultValue: () -> String?): String? = - if (this?.isEmpty() == true) defaultValue() else this - -fun String?.orDots(count: Int = 3): String { - return this ?: ("." * count) -} - -operator fun String.times(count: Int): String { - val builder = StringBuilder() - for (i in 0 until count) { - builder.append(this) - } - - return builder.toString() -} - -fun String.toast(context: Context, duration: Int = Toast.LENGTH_LONG) { - Toast.makeText(context, this, duration).show() -} - -context (Context) -fun String.toast(duration: Int = Toast.LENGTH_LONG) = toast(this@Context, duration) - -fun String.asUiText(): UiText = UiText.Simple(this) diff --git a/app/src/main/kotlin/com/meloda/fast/ext/ViewExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/ViewExt.kt deleted file mode 100644 index 4c131c38..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/ViewExt.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.meloda.fast.ext - -import android.content.Context -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.EditText -import android.widget.ImageView -import android.widget.TextView -import androidx.annotation.ColorInt -import androidx.annotation.Px -import androidx.appcompat.widget.Toolbar -import androidx.core.view.* -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout -import com.meloda.fast.R -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.databinding.ToolbarMenuItemAvatarBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide - -val EditText.trimmedText: String get() = text.toString().trim() -fun EditText.selectLast() { - setSelection(text.length) -} - -inline fun EditText.onDone(crossinline callback: () -> Unit) { - imeOptions = EditorInfo.IME_ACTION_DONE - maxLines = 1 - setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - callback.invoke() - return@setOnEditorActionListener true - } - false - } -} - -@Deprecated("use InsetManager") -fun View.showKeyboard(flags: Int = 0) { - (AppGlobal.Instance.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .showSoftInput(this, flags) -} - -@Deprecated("use InsetManager") -fun View.hideKeyboard(focusedView: View? = null, flags: Int = 0) { - (AppGlobal.Instance.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .hideSoftInputFromWindow(focusedView?.windowToken ?: this.windowToken, flags) -} - -fun TextInputLayout.clearError() { - if (error != null) error = null -} - -fun TextInputLayout.toggleError(errorText: String, isNeedToShow: Boolean) { - if (isNeedToShow) { - this.error = errorText - } else { - clearError() - } -} - -fun TextInputLayout.clearTextOnErrorIconClick(textField: TextInputEditText) { - setErrorIconOnClickListener { - textField.text = null - textField.showKeyboard() - } -} - -@JvmOverloads -fun ImageView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) { - visibility = if (drawable != null) View.VISIBLE else visibilityWhenFalse -} - -@JvmOverloads -fun TextView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) { - visibility = if (!text.isNullOrEmpty()) View.VISIBLE else visibilityWhenFalse -} - -fun View.setMarginsPx( - @Px leftMargin: Int? = null, - @Px topMargin: Int? = null, - @Px rightMargin: Int? = null, - @Px bottomMargin: Int? = null, -) { - (layoutParams as? ViewGroup.MarginLayoutParams)?.let { params -> - leftMargin?.run { params.leftMargin = this } - topMargin?.run { params.topMargin = this } - rightMargin?.run { params.rightMargin = this } - bottomMargin?.run { params.bottomMargin = this } - - requestLayout() - } -} - -fun TextView.clear() { - text = null -} - -fun View.invisible() = run { isInvisible = true } -fun View.visible() = run { isVisible = true } -fun View.gone() = run { isGone = true } - -@JvmOverloads -fun View.toggleVisibility(visible: Boolean?, visibilityWhenFalse: Int = View.GONE) = - run { visibility = if (visible == true) View.VISIBLE else visibilityWhenFalse } - -fun Toolbar.tintMenuItemIcons(@ColorInt colorToTint: Int) { - menu.forEach { item -> - item.icon?.setTint(colorToTint) - } -} - -fun Toolbar.addAvatarMenuItem(urlToLoad: String? = null, drawable: Drawable? = null): MenuItem { - val avatarMenuItemBinding = ToolbarMenuItemAvatarBinding.inflate( - LayoutInflater.from(context), null, false - ) - - val avatarMenuItem = menu.add(context.getString(R.string.navigation_profile)) - avatarMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) - avatarMenuItem.actionView = avatarMenuItemBinding.root - - val imageView = avatarMenuItemBinding.avatar - - when { - urlToLoad != null -> { - imageView.loadWithGlide { - imageUrl = urlToLoad - transformations = ImageLoader.userAvatarTransformations - } - } - - drawable != null -> { - imageView.loadWithGlide { - imageDrawable = drawable - transformations = ImageLoader.userAvatarTransformations - } - } - } - - return avatarMenuItem -} - -fun View.doOnApplyWindowInsets( - block: ( - view: View, - insets: WindowInsetsCompat, - paddings: Rect, - margins: Rect - ) -> WindowInsetsCompat -) { - val initialPaddings = recordInitialPaddingsForView(this) - val initialMargins = recordInitialMarginsForView(this) - - ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> - block(view, insets, initialPaddings, initialMargins) - } - - requestApplyInsetsWhenAttached() -} - -private fun recordInitialPaddingsForView(view: View) = - Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom) - -private fun recordInitialMarginsForView(view: View) = - Rect(view.marginStart, view.marginTop, view.marginEnd, view.marginBottom) - -fun View.requestApplyInsetsWhenAttached() { - if (isAttachedToWindow) { - requestApplyInsets() - } else { - doOnAttach { requestApplyInsets() } - } -} - -fun EditText.updateTextIfDiffer(text: String?) { - if (this.text?.toString() == text) return - setText(text) -} - -fun ViewGroup.bulkIsEnabled(isEnabled: Boolean) { - this.isEnabled = isEnabled - toggleChildrenIsEnabled(isEnabled) -} - -fun ViewGroup.toggleChildrenIsEnabled(isEnabled: Boolean) { - children.forEach { view -> view.toggleIsEnabled(isEnabled) } -} - -fun View.toggleIsEnabled(isEnabled: Boolean) { - this.isEnabled = isEnabled -} diff --git a/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt b/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt deleted file mode 100644 index 1eb415c4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.model - -import android.os.Parcelable -import androidx.room.Ignore -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -open class SelectableItem : Parcelable { - - @Ignore - @IgnoredOnParcel - var isSelected: Boolean = false - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/model/UpdateItem.kt b/app/src/main/kotlin/com/meloda/fast/model/UpdateItem.kt deleted file mode 100644 index e293551f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/UpdateItem.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.meloda.fast.model - -import android.os.Parcelable -import com.google.gson.Gson -import kotlinx.parcelize.Parcelize - -@Parcelize -data class UpdateItem( - val id: Int, - val versionName: String, - val versionCode: Int, - val mandatory: Int, - val changelog: String?, - val enabled: Int, - val fileName: String, - val date: Long, - val extension: String, - val originalName: String, - val fileSize: Int, - val preRelease: Int, - val downloadLink: String, -) : Parcelable { - - fun isMandatory(): Boolean = mandatory == 1 - fun isEnabled(): Boolean = enabled == 1 - fun isPreRelease(): Boolean = preRelease == 1 - - override fun toString(): String { - return Gson().toJson(this) - } - - companion object { - val EMPTY = UpdateItem( - id = 0, - versionName = "1.0.0", - versionCode = 2, - mandatory = 1, - changelog = "Some kind of simple changelog", - enabled = 1, - fileName = "bruhmeme.apk", - date = System.currentTimeMillis(), - extension = "", - originalName = "", - fileSize = 0, - preRelease = 0, - downloadLink = "https://c4.kemono.party/data/98/8c/988cf166f1ee9cd318e2407e6bfbabf60bffa53ed229ea0b2434009f1598e039.png?f=JessieGym002b4pt.png" - ) - } - -} - -@Parcelize -data class UpdateActualUrl(val url: String) : Parcelable diff --git a/app/src/main/kotlin/com/meloda/fast/model/base/AdapterDiffItem.kt b/app/src/main/kotlin/com/meloda/fast/model/base/AdapterDiffItem.kt deleted file mode 100644 index 015fde04..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/base/AdapterDiffItem.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.model.base - -interface AdapterDiffItem { - - val id: Int - - fun areItemsTheSame(newItem: AdapterDiffItem): Boolean { - return id == newItem.id - } - - fun areContentsTheSame(newItem: AdapterDiffItem): Boolean { - return this == newItem - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/model/base/DisplayableItem.kt b/app/src/main/kotlin/com/meloda/fast/model/base/DisplayableItem.kt deleted file mode 100644 index 96878c4e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/base/DisplayableItem.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.meloda.fast.model.base - -interface DisplayableItem diff --git a/app/src/main/kotlin/com/meloda/fast/model/base/UiImage.kt b/app/src/main/kotlin/com/meloda/fast/model/base/UiImage.kt deleted file mode 100644 index f5042622..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/base/UiImage.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.meloda.fast.model.base - -import android.content.Context -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.widget.ImageView -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat -import com.meloda.fast.ext.GlideParams -import com.meloda.fast.ext.ImageLoader.loadWithGlide - -sealed class UiImage { - - data class Resource(@DrawableRes val resId: Int) : UiImage() - - data class Simple(val drawable: Drawable?) : UiImage() - - data class Color(@ColorInt val color: Int) : UiImage() - - data class ColorResource(@ColorRes val resId: Int) : UiImage() - - data class Url(val url: String) : UiImage() - - fun extractUrl(): String? = when (this) { - is Url -> this.url - else -> null - } - - fun getResourceId(): Int? = when(this) { - is Resource -> this.resId - else -> null - } -} - -fun ImageView.setImage(image: UiImage, glideBlock: GlideParams.() -> Unit) { - val glideParams = GlideParams() - glideBlock.invoke(glideParams) - this.setImage(image, glideParams) -} - -fun ImageView.setImage(image: UiImage, glideParams: GlideParams? = null) { - image.attachTo(this, glideParams) -} - -fun UiImage?.attachTo(imageView: ImageView, glideBlock: GlideParams.() -> Unit) { - val glideParams = GlideParams() - glideBlock.invoke(glideParams) - this.attachTo(imageView, glideParams) -} - -fun UiImage?.attachTo(imageView: ImageView, glideParams: GlideParams? = null) { - when (this) { - is UiImage.Simple -> imageView.setImageDrawable(drawable) - is UiImage.Resource -> imageView.setImageResource(resId) - is UiImage.Color -> imageView.setImageDrawable(ColorDrawable(color)) - is UiImage.ColorResource -> imageView.setImageDrawable( - ColorDrawable(ContextCompat.getColor(imageView.context, resId)) - ) - - is UiImage.Url -> glideParams?.let { params -> - params.imageUrl = url - imageView.loadWithGlide(params) - } - - else -> Unit - } -} - -fun UiImage?.asDrawable(context: Context): Drawable? { - return when (this) { - is UiImage.Simple -> drawable - is UiImage.Resource -> ContextCompat.getDrawable(context, resId) - is UiImage.Color -> ColorDrawable(color) - is UiImage.ColorResource -> ColorDrawable(ContextCompat.getColor(context, resId)) - else -> null - } -} - -@Composable -fun UiImage?.getImage(): Any? { - val context = LocalContext.current - return when(this) { - is UiImage.Color -> ColorDrawable(color) - is UiImage.ColorResource -> ColorDrawable(ContextCompat.getColor(context, resId)) - is UiImage.Resource -> ContextCompat.getDrawable(context, resId) - is UiImage.Simple -> drawable - is UiImage.Url -> url - null -> null - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/receiver/StopLongPollServiceReceiver.kt b/app/src/main/kotlin/com/meloda/fast/receiver/StopLongPollServiceReceiver.kt deleted file mode 100644 index 6379f645..00000000 --- a/app/src/main/kotlin/com/meloda/fast/receiver/StopLongPollServiceReceiver.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.meloda.fast.receiver - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import androidx.core.app.NotificationManagerCompat -import androidx.core.content.edit -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.screens.main.activity.LongPollState -import com.meloda.fast.screens.main.activity.MainActivity -import com.meloda.fast.screens.settings.SettingsFragment -import kotlinx.coroutines.flow.update - -class StopLongPollServiceReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == ACTION_STOP) { - val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1) - - if (notificationId != -1) { - NotificationManagerCompat.from(context).cancel(notificationId) - } - - AppGlobal.preferences.edit { - putBoolean(SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, false) - } - - MainActivity.longPollState.update { LongPollState.Stop } - MainActivity.longPollState.update { LongPollState.DefaultService } - } - } - - companion object { - const val ACTION_STOP = "stop_long_poll" - const val NOTIFICATION_ID = "notification_id" - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/CaptchaScreens.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/CaptchaScreens.kt deleted file mode 100644 index e6542189..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/CaptchaScreens.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.screens.captcha - -import com.github.terrakok.cicerone.androidx.FragmentScreen -import com.meloda.fast.screens.captcha.presentation.CaptchaFragment - -object CaptchaScreens { - - fun captchaScreen() = FragmentScreen(key = "CaptchaScreen") { - CaptchaFragment.newInstance() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/di/CaptchaDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/di/CaptchaDI.kt deleted file mode 100644 index 90de93a8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/di/CaptchaDI.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.meloda.fast.screens.captcha.di - -import com.meloda.fast.di.navigationModule -import com.meloda.fast.screens.captcha.presentation.CaptchaCoordinator -import com.meloda.fast.screens.captcha.presentation.CaptchaCoordinatorImpl -import com.meloda.fast.screens.captcha.presentation.CaptchaViewModelImpl -import com.meloda.fast.screens.captcha.screen.CaptchaScreen -import com.meloda.fast.screens.captcha.validation.CaptchaValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.core.module.dsl.singleOf -import org.koin.core.qualifier.named -import org.koin.core.scope.Scope -import org.koin.dsl.bind -import org.koin.dsl.module - -val captchaModule = module { - val moduleQualifier = named("captcha") - - includes(navigationModule) - - single(moduleQualifier) { screen().resultFlow } - single { screen().getArguments() } - - single { - CaptchaCoordinatorImpl( - resultFlow = get(moduleQualifier), - router = get() - ) - } bind CaptchaCoordinator::class - - singleOf(::CaptchaValidator) - viewModelOf(::CaptchaViewModelImpl) -} - -private fun Scope.screen(): CaptchaScreen = get() - diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/model/CaptchaValidationResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/model/CaptchaValidationResult.kt deleted file mode 100644 index 6fddec93..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/model/CaptchaValidationResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.screens.captcha.model - -sealed class CaptchaValidationResult { - object Empty : CaptchaValidationResult() - object Valid : CaptchaValidationResult() - - fun isValid() = this == Valid -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaCoordinator.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaCoordinator.kt deleted file mode 100644 index 45164b7e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaCoordinator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.screens.captcha.presentation - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.screens.captcha.screen.CaptchaResult -import kotlinx.coroutines.flow.MutableSharedFlow - -interface CaptchaCoordinator { - - fun finishWithResult(result: CaptchaResult) -} - -class CaptchaCoordinatorImpl constructor( - val resultFlow: MutableSharedFlow, - val router: Router -) : CaptchaCoordinator { - - override fun finishWithResult(result: CaptchaResult) { - resultFlow.tryEmit(result) - router.exit() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaFragment.kt deleted file mode 100644 index 292da0f2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaFragment.kt +++ /dev/null @@ -1,234 +0,0 @@ -package com.meloda.fast.screens.captcha.presentation - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.addCallback -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil.request.ImageRequest -import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.screens.captcha.model.CaptchaScreenState -import com.meloda.fast.ui.* -import com.meloda.fast.ui.widgets.CoilImage -import com.meloda.fast.ui.widgets.TextFieldErrorText -import org.koin.androidx.viewmodel.ext.android.viewModel - -class CaptchaFragment : BaseFragment() { - - private val viewModel: CaptchaViewModel by viewModel() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - AppTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier - .statusBarsPadding() - .navigationBarsPadding() - .imePadding() - ) { - val state by viewModel.screenState.collectAsStateWithLifecycle() - - CaptchaScreen( - onCancelButtonClicked = viewModel::onCancelButtonClicked, - onCodeInputChanged = viewModel::onCodeInputChanged, - onTextFieldDoneClicked = viewModel::onTextFieldDoneClicked, - onDoneButtonClicked = viewModel::onDoneButtonClicked, - state = state - ) - } - } - } - } - - @Preview - @Composable - fun CaptchaScreenPreview() { - AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { - CaptchaScreen( - onCancelButtonClicked = {}, - onCodeInputChanged = {}, - onTextFieldDoneClicked = {}, - onDoneButtonClicked = {}, - state = CaptchaScreenState.EMPTY - ) - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun CaptchaScreen( - onCancelButtonClicked: () -> Unit, - onCodeInputChanged: (String) -> Unit, - onTextFieldDoneClicked: () -> Unit, - onDoneButtonClicked: () -> Unit, - state: CaptchaScreenState, - ) { - val focusManager = LocalFocusManager.current - - Column( - modifier = Modifier - .fillMaxSize() - .padding(30.dp), - verticalArrangement = Arrangement.SpaceBetween - ) { - ExtendedFloatingActionButton( - onClick = onCancelButtonClicked, - text = { - Text( - text = "Cancel", - color = MaterialTheme.colorScheme.primary - ) - }, - icon = { - Icon( - painter = painterResource(id = R.drawable.ic_round_close_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - } - ) - - Column( - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = "Captcha", - style = MaterialTheme.typography.displayMedium, - color = MaterialTheme.colorScheme.onBackground - ) - Spacer(modifier = Modifier.height(38.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "To proceed with your action, enter a code from the picture", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.weight(0.5f) - ) - Spacer(modifier = Modifier.width(24.dp)) - - CoilImage( - model = ImageRequest.Builder(LocalContext.current) - .data(state.captchaImage) - .crossfade(true) - .build(), - contentDescription = null, - modifier = Modifier - .border( - 2.dp, - MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(10.dp) - ) - .clip(RoundedCornerShape(10.dp)) - .height(48.dp) - .width(130.dp), - contentScale = ContentScale.FillBounds, - previewPainter = painterResource(id = R.drawable.test_captcha) - ) - } - - Spacer(modifier = Modifier.height(30.dp)) - - var code by remember { mutableStateOf(TextFieldValue(state.captchaCode)) } - val showError = state.codeError - - TextField( - value = code, - onValueChange = { newText -> - code = newText - onCodeInputChanged(newText.text) - }, - label = { Text(text = "Code") }, - placeholder = { Text(text = "Code") }, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)), - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.round_qr_code_24), - contentDescription = null, - tint = if (showError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - } - ) - }, - shape = RoundedCornerShape(10.dp), - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions( - onDone = { - focusManager.clearFocus() - onTextFieldDoneClicked() - } - ), - isError = showError - ) - - AnimatedVisibility(visible = showError) { - TextFieldErrorText(text = "Field must not be empty") - } - } - - FloatingActionButton( - onClick = onDoneButtonClicked, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_round_done_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - activity?.onBackPressedDispatcher?.addCallback { - viewModel.onBackButtonClicked() - } - } - - companion object { - - fun newInstance(): CaptchaFragment { - return CaptchaFragment() - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaArguments.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaArguments.kt deleted file mode 100644 index 751f2223..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaArguments.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.meloda.fast.screens.captcha.screen - -data class CaptchaArguments(val captchaSid: String, val captchaImage: String) diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaResult.kt deleted file mode 100644 index 1504a66e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.screens.captcha.screen - -sealed class CaptchaResult { - object Cancelled : CaptchaResult() - data class Success(val sid: String, val code: String) : CaptchaResult() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaScreen.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaScreen.kt deleted file mode 100644 index 7aeca346..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaScreen.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.screens.captcha.screen - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.base.screen.AppScreen -import com.meloda.fast.base.screen.createResultFlow -import com.meloda.fast.screens.captcha.CaptchaScreens -import kotlin.properties.Delegates - -class CaptchaScreen : AppScreen { - - override val resultFlow = createResultFlow() - - override var args: CaptchaArguments by Delegates.notNull() - - override fun show(router: Router, args: CaptchaArguments) { - this.args = args - router.navigateTo(CaptchaScreens.captchaScreen()) - } -} - - diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoFragment.kt deleted file mode 100644 index 1131d301..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoFragment.kt +++ /dev/null @@ -1,299 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import by.kirich1409.viewbindingdelegate.viewBinding -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.tabs.TabLayoutMediator -import com.meloda.fast.R -import com.meloda.fast.api.model.VkChat -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.base.viewmodel.BaseViewModelFragment -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.databinding.FragmentChatInfoBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.getParcelableCompat -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.orDots -import com.meloda.fast.ext.visible -import com.meloda.fast.screens.messages.MessagesHistoryFragment -import dev.chrisbanes.insetter.applyInsetter -import org.koin.androidx.viewmodel.ext.android.viewModel -import java.text.SimpleDateFormat -import java.util.Locale - -class ChatInfoFragment : BaseViewModelFragment(R.layout.fragment_chat_info) { - - companion object { - const val KeyConfirmRemoveChatUser = "confirm_remove_chat_user" - const val KeyRemoveChatUser = "remove_chat_user" - const val ArgMemberId = "member_id" - - private const val ArgConversation = "conversation" - private const val ArgUser = "user" - private const val ArgGroup = "group" - - fun newInstance( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup?, - ): ChatInfoFragment { - val fragment = ChatInfoFragment() - fragment.arguments = bundleOf( - ArgConversation to conversation, - ArgUser to user, - ArgGroup to group - ) - - return fragment - } - } - - override val viewModel: ChatInfoViewModel by viewModel() - - private val binding by viewBinding(FragmentChatInfoBinding::bind) - - private val user: VkUser? by lazy { - requireArguments().getParcelableCompat(MessagesHistoryFragment.ARG_USER, VkUser::class.java) - } - - private val group: VkGroup? by lazy { - requireArguments().getParcelableCompat( - MessagesHistoryFragment.ARG_GROUP, - VkGroup::class.java - ) - } - - private val conversation: VkConversationDomain by lazy { - requireNotNull( - requireArguments().getParcelableCompat( - MessagesHistoryFragment.ARG_CONVERSATION, - VkConversationDomain::class.java - ) - ) - } - - private val chatProfiles: MutableList = mutableListOf() - private val chatGroups: MutableList = mutableListOf() - private val chatMembers: MutableList = mutableListOf() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewModel.getConversationMembers(conversation.id) - - val title = when { - conversation.isChat() -> conversation.conversationTitle - conversation.isUser() -> user?.toString() - conversation.isGroup() -> group?.name - else -> null - } - - - binding.toolbar.applyInsetter { - type(statusBars = true) { padding() } - } - binding.progresBar.applyInsetter { - type(navigationBars = true) { padding() } - } - binding.toolbar.title = title.orDots() - - updateStatus() - - val avatar = when { - conversation.isUser() -> user?.photo200 - conversation.isGroup() -> group?.photo200 - conversation.isChat() -> conversation.conversationPhoto - else -> null - } - - val avatarImageView = binding.toolbar.avatarImageView - avatarImageView.visible() - avatarImageView.loadWithGlide { - imageUrl = avatar - asCircle = true - crossFade = true - } - - binding.toolbar.avatarClickAction = { - showAvatarOptions() - } - binding.toolbar.startButtonClickAction = - { requireActivity().onBackPressedDispatcher.onBackPressed() } - - binding.viewPager.offscreenPageLimit = getTabsCount() - 1 - - childFragmentManager.setFragmentResultListener( - KeyConfirmRemoveChatUser, - this - ) { _, bundle -> - val memberId = bundle.getInt(ArgMemberId) - showConfirmRemoveMemberAlert(memberId) - } - } - - override fun onEvent(event: VkEvent) { - super.onEvent(event) - - when (event) { - is GetConversationMembersEvent -> { - fillChatInfo(event) - } - - is RemoveChatUserEvent -> { - val memberId = event.memberId - childFragmentManager.setFragmentResult( - KeyRemoveChatUser, bundleOf( - ArgMemberId to memberId - ) - ) - } - } - } - - // TODO: 17.04.2023, Danil Nikolaev: handle loading - private fun onProgressStart() { - binding.tabs.gone() - binding.viewPager.gone() - binding.progresBar.visible() - } - - private fun onProgressStop() { - binding.tabs.visible() - binding.viewPager.visible() - binding.progresBar.gone() - } - - private fun fillChatInfo(event: GetConversationMembersEvent) { - val onlineMembers = event.profiles.filter { it.online } - updateStatus(onlineMembers.size) - - val eventChatMembers = event.items.map { vkChatMember -> - val memberUser: VkUser? = if (vkChatMember.memberId < 0) null - else event.profiles.firstOrNull { it.id == vkChatMember.memberId } - - val memberGroup: VkGroup? = if (vkChatMember.memberId > 0) null - else event.groups.firstOrNull { it.id == vkChatMember.memberId } - - VkChat.ChatMember( - id = vkChatMember.memberId, - type = if (vkChatMember.memberId > 0) VkChat.ChatMember.ChatMemberType.Profile else VkChat.ChatMember.ChatMemberType.Group, - isOnline = memberUser?.online, - lastSeen = memberUser?.lastSeen, - name = memberGroup?.name, - firstName = memberUser?.firstName, - lastName = memberUser?.lastName, - invitedBy = vkChatMember.invitedBy, - photo50 = null, - photo100 = null, - photo200 = memberUser?.photo200 ?: memberGroup?.photo200, - isOwner = vkChatMember.isOwner, - isAdmin = vkChatMember.isAdmin, - canKick = vkChatMember.canKick - ) - } - - chatProfiles.addAll(event.profiles) - chatGroups.addAll(event.groups) - chatMembers.addAll(eventChatMembers) - prepareTabs() - } - - private fun updateStatus(onlineMembersCount: Int? = null) { - val status = when { - conversation.isChat() -> { - val membersCountText = "${conversation.membersCount} members" - if (onlineMembersCount == null) membersCountText - else { - "$membersCountText, $onlineMembersCount online" - } - } - - conversation.isUser() -> when { - // TODO: 9/15/2021 user normal time - user?.online == true -> "Online" - user?.lastSeen != null -> "Last seen at ${ - SimpleDateFormat( - "HH:mm", - Locale.getDefault() - ).format(user?.lastSeen!! * 1000L) - }" - - else -> if (user?.lastSeenStatus != null) "Last seen ${user?.lastSeenStatus!!}" else "Last seen recently" - } - - conversation.isGroup() -> if (group?.membersCount != null) "${group?.membersCount} members" else "Group" - else -> null - } - - binding.toolbar.subtitle = status.orDots() - - } - - fun getTabsCount(): Int { - return if (conversation.isChat()) 6 else 5 - } - - fun createTabFragment(position: Int): Fragment { - if (conversation.isChat() && position == 0) { - return ChatInfoMembersFragment.newInstance( - chatProfiles, - chatGroups, - chatMembers - ) - } - - return Fragment() - } - - private fun prepareTabs() { - val titles = mutableListOf("Members", "Photos", "Videos", "Audios", "Files", "Links") - - if (!conversation.isChat()) { - titles.removeAt(0) - } - - binding.viewPager.adapter = ChatInfoPagerAdapter(this) - - TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position -> - tab.text = titles[position] - }.attach() - } - - private fun showConfirmRemoveMemberAlert(memberId: Int) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage(R.string.confirm_remove_chat_user) - .setPositiveButton(R.string.yes) { _, _ -> - viewModel.removeChatUser(conversation.localId, memberId) - } - .setNegativeButton(R.string.no, null) - .show() - } - - private fun showAvatarOptions() { - val options = mutableListOf("Open") - - if (conversation.canChangeInfo) { - options += listOf("Edit", "Delete") - } - - MaterialAlertDialogBuilder(requireContext()) - .setItems(options.toTypedArray()) { _, which -> - when (options[which]) { - "Open" -> { - Toast.makeText(requireContext(), "Open photo", Toast.LENGTH_SHORT).show() - } - - else -> - Toast.makeText(requireContext(), "Change info", Toast.LENGTH_SHORT).show() - } - } - .show() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersAdapter.kt deleted file mode 100644 index b9a45f04..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersAdapter.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DiffUtil -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.model.VkChat -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.base.adapter.BaseAdapter -import com.meloda.fast.base.adapter.BaseHolder -import com.meloda.fast.databinding.ItemChatMemberBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.toggleVisibility -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.Objects - -class ChatInfoMembersAdapter( - context: Context, - preAddedValues: List, - private val profiles: List, - private val groups: List, - private val confirmRemoveMemberAction: ((memberId: Int) -> Unit)? = null, -) : BaseAdapter( - context, - comparator, - preAddedValues -) { - - companion object { - val comparator = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: VkChat.ChatMember, - newItem: VkChat.ChatMember, - ): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame( - oldItem: VkChat.ChatMember, - newItem: VkChat.ChatMember, - ): Boolean { - return Objects.deepEquals(oldItem, newItem) - } - - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return Holder(ItemChatMemberBinding.inflate(inflater, parent, false)) - } - - inner class Holder( - private val binding: ItemChatMemberBinding, - ) : BaseHolder(binding.root) { - - private val colorOnBackground = ContextCompat.getColor(context, R.color.colorOnBackground) - private val colorPrimary = ContextCompat.getColor(context, R.color.colorPrimary) - - override fun bind(position: Int) { - val chatMember = getItem(position) - - binding.avatar.loadWithGlide { - imageUrl = chatMember.photo200 - crossFade = true - placeholderColor = Color.GRAY - errorColor = Color.RED - } - - val title = chatMember.name ?: "${chatMember.firstName} ${chatMember.lastName}" - binding.title.text = title - - binding.online.toggleVisibility(chatMember.isProfile()) - binding.online.text = - if (chatMember.isOnline == true) "Online" - else if (chatMember.lastSeen != null) "Last seen at ${ - SimpleDateFormat( - "HH:mm", - Locale.getDefault() - ).format(chatMember.lastSeen * 1000L) - }" - else "Offline" - - binding.star.toggleVisibility(chatMember.isAdmin || chatMember.isOwner) - binding.star.imageTintList = - ColorStateList.valueOf( - if (chatMember.isOwner) colorPrimary - else colorOnBackground - ) - - binding.remove.toggleVisibility( - chatMember.canKick || chatMember.id == UserConfig.userId - ) - binding.remove.setOnClickListener { confirmRemoveMemberAction?.invoke(chatMember.id) } - } - } - - fun searchMemberIndex(memberId: Int): Int? { - for (i in indices) { - val member = getItem(i) - if (member.id == memberId) return i - } - - return null - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersFragment.kt deleted file mode 100644 index 5c54c835..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersFragment.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import android.os.Bundle -import android.view.View -import androidx.core.os.bundleOf -import androidx.fragment.app.setFragmentResult -import androidx.fragment.app.setFragmentResultListener -import by.kirich1409.viewbindingdelegate.viewBinding -import com.meloda.fast.R -import com.meloda.fast.api.model.VkChat -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.databinding.FragmentChatInfoMembersBinding -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.view.SpaceItemDecoration -import dev.chrisbanes.insetter.applyInsetter - -class ChatInfoMembersFragment : BaseFragment(R.layout.fragment_chat_info_members) { - - companion object { - - private const val ArgProfiles = "profiles" - private const val ArgGroups = "groups" - private const val ArgMembers = "members" - - fun newInstance( - profiles: List, - groups: List, - members: List - ): ChatInfoMembersFragment { - val fragment = ChatInfoMembersFragment() - fragment.arguments = bundleOf( - ArgProfiles to profiles, - ArgGroups to groups, - ArgMembers to members - ) - - return fragment - } - } - - private val binding by viewBinding(FragmentChatInfoMembersBinding::bind) - - @Suppress("UNCHECKED_CAST") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.recyclerView.applyInsetter { - type(navigationBars = true) { padding() } - } - binding.recyclerView.addItemDecoration(SpaceItemDecoration(topMargin = 8.dpToPx())) - - // TODO: 17.04.2023, Danil Nikolaev: use single data class as parcelable - val profiles = requireArguments().getSerializable(ArgProfiles) as List - val groups = requireArguments().getSerializable(ArgGroups) as List - val members = requireArguments().getSerializable(ArgMembers) as List - - val adapter = - ChatInfoMembersAdapter( - requireContext(), - members, - profiles, - groups, - confirmRemoveMemberAction = { memberId -> - setFragmentResult( - ChatInfoFragment.KeyConfirmRemoveChatUser, - bundleOf(ChatInfoFragment.ArgMemberId to memberId) - ) - } - ) - binding.recyclerView.adapter = adapter - binding.recyclerView.itemAnimator = null - - setFragmentResultListener(ChatInfoFragment.KeyRemoveChatUser) { _, bundle -> - val memberId = bundle.getInt(ChatInfoFragment.ArgMemberId) - adapter.searchMemberIndex(memberId)?.let { index -> - adapter.removeAt(index) - } - } - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoPagerAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoPagerAdapter.kt deleted file mode 100644 index a93a7050..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoPagerAdapter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter - -class ChatInfoPagerAdapter( - private val fragment: ChatInfoFragment -) : FragmentStateAdapter(fragment) { - - override fun getItemCount(): Int { - return fragment.getTabsCount() - } - - override fun createFragment(position: Int): Fragment { - return fragment.createTabFragment(position) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoViewModel.kt deleted file mode 100644 index 3bf2e76c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoViewModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import androidx.lifecycle.viewModelScope -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.model.VkChatMember -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.data.messages.MessagesRepository -import kotlinx.coroutines.launch - -class ChatInfoViewModel constructor( - private val messagesRepository: MessagesRepository -) : DeprecatedBaseViewModel() { - - fun getConversationMembers(peerId: Int) = viewModelScope.launch { - makeJob( - { - messagesRepository.getConversationMembers( - peerId, - extended = true, - fields = VKConstants.ALL_FIELDS - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - - val items = response.items.map { member -> member.asVkChatMember() } - val profiles = response.profiles.orEmpty().map { profile -> profile.mapToDomain() } - val groups = response.groups.orEmpty().map { group -> group.mapToDomain() } - - sendEvent(GetConversationMembersEvent(response.count, items, profiles, groups)) - } - ) - } - - fun removeChatUser(chatId: Int, memberId: Int) = viewModelScope.launch { - makeJob( - { messagesRepository.removeChatUser(chatId, memberId) }, - onAnswer = { - sendEvent(RemoveChatUserEvent(chatId, memberId)) - } - ) - } - -} - -data class GetConversationMembersEvent( - val count: Int, - val items: List, - val profiles: List, - val groups: List -) : VkEvent() - -data class RemoveChatUserEvent( - val chatId: Int, val memberId: Int -) : VkEvent() diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/di/ChatInfoDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/di/ChatInfoDI.kt deleted file mode 100644 index ac1f6db7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/di/ChatInfoDI.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.chatinfo.di - -import com.meloda.fast.screens.chatinfo.ChatInfoViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val chatInfoModule = module { - viewModelOf(::ChatInfoViewModel) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationCompose.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationCompose.kt deleted file mode 100644 index 1a094176..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationCompose.kt +++ /dev/null @@ -1,256 +0,0 @@ -package com.meloda.fast.screens.conversations - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.surfaceColorAtElevation -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.model.presentation.VkConversationUi -import com.meloda.fast.ext.combinedClickableSound -import com.meloda.fast.ext.getString -import com.meloda.fast.ext.orDots -import com.meloda.fast.model.base.getImage -import com.meloda.fast.ui.widgets.CoilImage - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun Conversation( - onItemClick: (VkConversationUi) -> Unit, - onItemLongClick: (VkConversationUi) -> Unit, - conversation: VkConversationUi, - maxLines: Int -) { - Box( - modifier = Modifier - .fillMaxWidth() - .combinedClickableSound( - onClick = { onItemClick(conversation) }, - onLongClick = { onItemLongClick(conversation) } - ) - ) { - if (conversation.isUnread) { - Box( - modifier = Modifier - .matchParentSize() - .padding(start = 8.dp) - .clip( - RoundedCornerShape( - topStart = 34.dp, - bottomStart = 34.dp - ) - ) - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)) - ) - } - - Column(modifier = Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.height(8.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.width(16.dp)) - Box(modifier = Modifier.size(56.dp)) { - - if (conversation.id == UserConfig.userId) { - Box( - modifier = Modifier - .fillMaxSize() - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) - ) { - Image( - modifier = Modifier - .align(Alignment.Center) - .size(32.dp), - painter = painterResource(id = R.drawable.ic_round_bookmark_border_24), - contentDescription = null - ) - } - } else { - Image( - modifier = Modifier - .fillMaxSize() - .clip(CircleShape), - painter = painterResource(id = R.drawable.ic_account_circle_cut), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.outline) - ) - CoilImage( - modifier = Modifier - .fillMaxSize() - .clip(CircleShape), - contentDescription = null, - model = conversation.avatar.getImage(), - previewPainter = painterResource(id = R.drawable.ic_account_circle_cut), - ) - } - - if (conversation.isPinned) { - Box( - modifier = Modifier - .clip(CircleShape) - .defaultMinSize(minWidth = 20.dp, minHeight = 20.dp) - .background(MaterialTheme.colorScheme.outline) - ) { - Image( - modifier = Modifier - .height(14.dp) - .align(Alignment.Center), - painter = painterResource(id = R.drawable.ic_round_push_pin_24), - contentDescription = null - ) - } - } - - if (conversation.isOnline) { - Box( - modifier = Modifier - .clip(CircleShape) - .size(18.dp) - .background( - if (conversation.isUnread) { - MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) - } else { - MaterialTheme.colorScheme.background - } - ) - .padding(2.dp) - .align(Alignment.BottomEnd) - ) { - Box( - modifier = Modifier - .clip(CircleShape) - .matchParentSize() - .background(MaterialTheme.colorScheme.primary) - ) - } - } - - if (conversation.isBirthday) { - Box( - modifier = Modifier - .clip(CircleShape) - .size(16.dp) - .background( - if (conversation.isUnread) { - MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) - } else { - MaterialTheme.colorScheme.background - } - ) - .padding(2.dp) - .align(Alignment.TopEnd) - ) { - Box( - modifier = Modifier - .clip(CircleShape) - .matchParentSize() - .background(Color(0xFFB00B69)) - ) { - Image( - modifier = Modifier - .align(Alignment.Center) - .size(10.dp), - painter = painterResource(id = R.drawable.round_cake_24), - contentDescription = null - ) - } - } - } - } - - Spacer(modifier = Modifier.width(16.dp)) - - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = conversation.title.getString().orDots(), - modifier = Modifier, - minLines = 1, - maxLines = maxLines, - style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp) - ) - - Row { - conversation.attachmentImage?.getResourceId()?.let { resId -> - Column { - Spacer(modifier = Modifier.height(4.dp)) - Image( - modifier = Modifier.size(14.dp), - painter = painterResource(id = resId), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer) - ) - } - - Spacer(modifier = Modifier.width(2.dp)) - } - - Text( - modifier = Modifier.weight(1f), - text = conversation.message, - minLines = 1, - maxLines = maxLines, - style = MaterialTheme.typography.bodyLarge, - overflow = TextOverflow.Ellipsis - ) - } - } - - Spacer(modifier = Modifier.width(4.dp)) - Column { - Text(text = conversation.date) - - conversation.unreadCount?.let { count -> - Spacer(modifier = Modifier.height(6.dp)) - Box( - modifier = Modifier - .clip(CircleShape) - .defaultMinSize(minWidth = 20.dp, minHeight = 20.dp) - .background(MaterialTheme.colorScheme.primary) - .align(Alignment.CenterHorizontally) - ) { - Text( - modifier = Modifier - .padding(2.dp) - .align(Alignment.Center), - text = count, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary - ) - } - } - } - - Spacer(modifier = Modifier.width(24.dp)) - } - - Spacer(modifier = Modifier.height(8.dp)) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt deleted file mode 100644 index 60779e60..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt +++ /dev/null @@ -1,342 +0,0 @@ -package com.meloda.fast.screens.conversations - -import android.os.Bundle -import android.view.HapticFeedbackConstants -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideIn -import androidx.compose.animation.slideOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.meloda.fast.R -import com.meloda.fast.api.model.presentation.VkConversationUi -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.asUiText -import com.meloda.fast.ext.listenValue -import com.meloda.fast.ext.showDialog -import com.meloda.fast.ext.string -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.ui.AppTheme -import org.koin.androidx.viewmodel.ext.android.viewModel - -class ConversationsFragment : BaseFragment(R.layout.fragment_conversations) { - - private val viewModel: ConversationsViewModel by viewModel() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - listenViewModel() - - (view as? ComposeView)?.setContent { - ConversationsScreen() - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun ConversationsScreen() { - val conversations by viewModel.conversationsList.collectAsStateWithLifecycle() - val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() - - val useLargeTopAppBar = AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_LARGE_TOP_APP_BAR, - SettingsFragment.DEFAULT_VALUE_USE_LARGE_TOP_APP_BAR - ) - val useMultiline = AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_APPEARANCE_MULTILINE, - SettingsFragment.DEFAULT_VALUE_MULTILINE - ) - val topAppBarState = rememberTopAppBarState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState) - - val scaffoldModifier = if (useLargeTopAppBar) { - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - } else { - Modifier.fillMaxSize() - } - - val lazyListState = rememberLazyListState() - - AppTheme { - Scaffold( - modifier = scaffoldModifier, - topBar = { - var dropDownMenuExpanded by remember { - mutableStateOf(false) - } - - val actions: @Composable RowScope.() -> Unit = @Composable { - IconButton( - onClick = { - dropDownMenuExpanded = true - } - ) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = "Options" - ) - } - - DropdownMenu( - expanded = dropDownMenuExpanded, - onDismissRequest = { - dropDownMenuExpanded = false - }, - offset = DpOffset(x = 0.dp, y = (-60).dp) - ) { - DropdownMenuItem( - onClick = { - viewModel.onToolbarMenuItemClicked(R.id.settings) - dropDownMenuExpanded = false - }, - text = { - Text(text = "Settings") - } - ) - DropdownMenuItem( - onClick = { - viewModel.onRefresh() - dropDownMenuExpanded = false - }, - text = { - Text(text = "Refresh") - } - ) - } - } - - val title = @Composable { - Text(text = if (isLoading) "Loading..." else "Conversations") - } - - if (useLargeTopAppBar) { - LargeTopAppBar( - title = title, - scrollBehavior = scrollBehavior, - actions = actions - ) - } else { - TopAppBar( - title = title, - actions = actions - ) - } - }, - floatingActionButton = { - if (!isLoading || conversations.isNotEmpty()) { - AnimatedVisibility( - visible = !lazyListState.isScrollInProgress || conversations.isNotEmpty(), - enter = slideIn(initialOffset = { IntOffset(x = 0, y = 300) }), - exit = slideOut(targetOffset = { IntOffset(x = 0, y = 300) }) - ) { - FloatingActionButton( - onClick = { - view?.performHapticFeedback(HapticFeedbackConstants.REJECT) - } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_baseline_create_24), - contentDescription = null - ) - } - } - } - } - ) { padding -> - if (isLoading && conversations.isEmpty()) { - Loader() - } else { - ConversationsList( - conversations = conversations, - padding = padding, - state = lazyListState, - useMultiline = useMultiline, - ) - } - } - } - } - - @Composable - fun Loader() { - AppTheme { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - } - - @Composable - fun ConversationsList( - conversations: List, - padding: PaddingValues, - state: LazyListState, - useMultiline: Boolean, - ) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(padding), - state = state - ) { - items( - count = conversations.size, - key = { index -> - val item = conversations[index] - item.conversationId - } - ) { index -> - Conversation( - onItemClick = viewModel::onConversationItemClick, - onItemLongClick = viewModel::onConversationItemLongClick, - conversation = conversations[index], - maxLines = if (useMultiline) 2 else 1, - ) - - if (index < conversations.size - 1) { - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } - - // TODO: 05.08.2023, Danil Nikolaev: remove and use compose dialogs - private fun listenViewModel() = with(viewModel) { - isNeedToShowOptionsDialog.listenValue(::showOptionsDialog) - isNeedToShowDeleteDialog.listenValue(::showDeleteConversationDialog) - isNeedToShowPinDialog.listenValue(::showPinConversationDialog) - } - - // TODO: 06.04.2023, Danil Nikolaev: extract creating options to VM - private fun showOptionsDialog(conversation: VkConversationUi?) { - if (conversation == null) return - - var canPinOneMoreDialog = true - if (viewModel.conversationsList.value.size > 4) { - if (viewModel.pinnedConversationsCount.value == 5 && !conversation.isPinned) { - canPinOneMoreDialog = false - } - } - - val read = "Mark as read" - - val pin = string( - if (conversation.isPinned) R.string.conversation_context_action_unpin - else R.string.conversation_context_action_pin - ) - - val delete = string(R.string.conversation_context_action_delete) - - val params = mutableListOf>() - - conversation.lastMessage?.run { - if (!conversation.isUnread && !this.isOut) { - params += "read" to read - } - - if (!this.text.isNullOrBlank()) { - params += "share" to "Share" - } - } - - if (canPinOneMoreDialog) params += "pin" to pin - - params += "delete" to delete - - context?.showDialog( - items = params.map { param -> param.second.asUiText() }, - itemsClickAction = { index, _ -> - val key = params[index].first - viewModel.onOptionsDialogOptionClicked(conversation, key) - }, - onDismissAction = viewModel::onOptionsDialogDismissed - ) - } - - private fun showDeleteConversationDialog(conversationId: Int?) { - if (conversationId == null) return - - context?.showDialog( - title = UiText.Resource(R.string.confirm_delete_conversation), - positiveText = UiText.Resource(R.string.action_delete), - positiveAction = { viewModel.onDeleteDialogPositiveClick(conversationId) }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onDeleteDialogDismissed - ) - } - - private fun showPinConversationDialog(conversation: VkConversationUi?) { - if (conversation == null) return - - context?.showDialog( - title = UiText.Resource( - if (conversation.isPinned) R.string.confirm_unpin_conversation - else R.string.confirm_pin_conversation - ), - positiveText = UiText.Resource( - if (conversation.isPinned) R.string.action_unpin - else R.string.action_pin - ), - positiveAction = { - viewModel.onPinDialogPositiveClick(conversation) - }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onPinDialogDismissed - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceProvider.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceProvider.kt deleted file mode 100644 index 5c618fff..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.screens.conversations - -import android.content.Context -import com.meloda.fast.R -import com.meloda.fast.base.ResourceProvider - -class ConversationsResourceProvider(context: Context) : ResourceProvider(context) { - - val colorPrimary = getColor(R.color.colorPrimary) - val colorOutline = getColor(R.color.colorOutline) - val colorOnPrimary = getColor(R.color.colorOnPrimary) - val colorUserAvatarAction = getColor(R.color.colorUserAvatarAction) - val colorOnUserAvatarAction = getColor(R.color.colorOnUserAvatarAction) - val colorBackground = getColor(R.color.colorBackground) - val colorBackgroundVariant = getColor(R.color.colorBackgroundVariant) - - val icLauncherColor = getColor(R.color.a1_500) - - val youPrefix = getString(R.string.you_message_prefix) - - val conversationUnreadBackground get() = getDrawable(R.drawable.ic_message_unread) - - val iconForwardedMessages = getDrawable(R.drawable.ic_attachment_forwarded_messages) - val iconForwardedMessage = getDrawable(R.drawable.ic_attachment_forwarded_message) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt deleted file mode 100644 index 04604649..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt +++ /dev/null @@ -1,573 +0,0 @@ -package com.meloda.fast.screens.conversations - -import androidx.lifecycle.viewModelScope -import coil.ImageLoader -import coil.request.ImageRequest -import com.github.terrakok.cicerone.Router -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.longpoll.LongPollEvent -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.base.BaseVkGroup -import com.meloda.fast.api.model.base.BaseVkUser -import com.meloda.fast.api.model.data.BaseVkConversation -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.model.presentation.VkConversationUi -import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest -import com.meloda.fast.api.network.conversations.ConversationsGetRequest -import com.meloda.fast.api.network.conversations.ConversationsPinRequest -import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest -import com.meloda.fast.api.network.users.UsersGetRequest -import com.meloda.fast.base.viewmodel.BaseViewModel -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.Screens -import com.meloda.fast.data.conversations.ConversationsRepository -import com.meloda.fast.data.messages.MessagesRepository -import com.meloda.fast.data.users.UsersRepository -import com.meloda.fast.ext.emitOnMainScope -import com.meloda.fast.ext.emitWithMain -import com.meloda.fast.ext.findIndex -import com.meloda.fast.ext.toMap -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -// TODO: 04.08.2023, Danil Nikolaev: calculate time here and give ui ready to be shown date - -interface ConversationsViewModel { - - val pinnedConversationsCount: StateFlow - - val conversationsList: StateFlow> - val isLoading: StateFlow - - val isNeedToShowOptionsDialog: StateFlow - val isNeedToShowDeleteDialog: StateFlow - val isNeedToShowPinDialog: StateFlow - - val profiles: StateFlow> - val groups: StateFlow> - - fun onOptionsDialogDismissed() - - fun onOptionsDialogOptionClicked(conversation: VkConversationUi, key: String): Boolean - - fun onDeleteDialogDismissed() - - fun onDeleteDialogPositiveClick(conversationId: Int) - - fun onRefresh() - - fun onConversationItemClick(conversationUi: VkConversationUi) - fun onConversationItemLongClick(conversationUi: VkConversationUi): Boolean - - fun onPinDialogDismissed() - fun onPinDialogPositiveClick(conversation: VkConversationUi) - fun onToolbarMenuItemClicked(itemId: Int): Boolean -} - -class ConversationsViewModelImpl constructor( - private val conversationsRepository: ConversationsRepository, - private val usersRepository: UsersRepository, - updatesParser: LongPollUpdatesParser, - private val router: Router, - private val messagesRepository: MessagesRepository, -) : ConversationsViewModel, BaseViewModel() { - - private val dataConversations: MutableStateFlow> = - MutableStateFlow(emptyList()) - - private val domainConversations: MutableStateFlow> = - MutableStateFlow(emptyList()) - - override val conversationsList: StateFlow> = - domainConversations.map { list -> - list.map(VkConversationDomain::mapToPresentation) - }.stateIn(viewModelScope, SharingStarted.Lazily, initialValue = emptyList()) - - override val isLoading = MutableStateFlow(false) - - override val isNeedToShowOptionsDialog = MutableStateFlow(null) - override val isNeedToShowDeleteDialog = MutableStateFlow(null) - override val isNeedToShowPinDialog = MutableStateFlow(null) - - override val profiles: MutableStateFlow> = MutableStateFlow(hashMapOf()) - override val groups: MutableStateFlow> = MutableStateFlow(hashMapOf()) - - override val pinnedConversationsCount = domainConversations.map { conversations -> - val pinnedConversations = conversations.filter { it.isPinned() } - pinnedConversations.size - }.stateIn(viewModelScope, SharingStarted.Eagerly, 0) - - private val imageLoader by lazy { - ImageLoader.Builder(AppGlobal.Instance) - .crossfade(true) - .build() - } - - override fun onOptionsDialogDismissed() { - isNeedToShowOptionsDialog.emitOnMainScope(null) - } - - override fun onOptionsDialogOptionClicked( - conversation: VkConversationUi, - key: String - ): Boolean { - return when (key) { - "read" -> { - readConversation( - conversationId = conversation.conversationId, - startMessageId = conversation.lastMessageId - ) - true - } - - "delete" -> { - isNeedToShowDeleteDialog.emitOnMainScope(conversation.id) - true - } - - "pin" -> { - isNeedToShowPinDialog.emitOnMainScope(conversation) - true - } - - else -> false - } - } - - override fun onDeleteDialogDismissed() { - isNeedToShowDeleteDialog.emitOnMainScope(null) - } - - override fun onDeleteDialogPositiveClick(conversationId: Int) { - deleteConversation(conversationId) - } - - override fun onRefresh() { - loadConversations() - } - - override fun onConversationItemClick(conversationUi: VkConversationUi) { - openMessagesHistoryScreen( - conversationUi, - conversationUi.conversationUser, - conversationUi.conversationGroup - ) - } - - override fun onConversationItemLongClick(conversationUi: VkConversationUi): Boolean { - val domainConversation = conversationsList.value.find { it.id == conversationUi.id } - isNeedToShowOptionsDialog.emitOnMainScope(domainConversation) - return true - } - - override fun onPinDialogDismissed() { - isNeedToShowPinDialog.emitOnMainScope(null) - } - - override fun onPinDialogPositiveClick(conversation: VkConversationUi) { - pinConversation(conversation.id, !conversation.isPinned) - } - - override fun onToolbarMenuItemClicked(itemId: Int): Boolean { - return when (itemId) { - R.id.settings -> { - router.navigateTo(Screens.Settings()) - true - } - - else -> false - } - } - - init { - updatesParser.onNewMessage(::handleNewMessage) - updatesParser.onMessageEdited(::handleEditedMessage) - updatesParser.onMessageIncomingRead(::handleReadIncomingMessage) - updatesParser.onMessageOutgoingRead(::handleReadOutgoingMessage) - updatesParser.onConversationPinStateChanged(::handlePinStateChanged) - - loadProfileUser() - loadConversations() - } - - private fun loadConversations(offset: Int? = null) { - viewModelScope.launch(Dispatchers.IO) { - isLoading.emitWithMain(true) - sendRequest( - onError = { - isLoading.emitWithMain(false) - false - } - ) { - conversationsRepository.get( - ConversationsGetRequest( - count = 30, - extended = true, - offset = offset, - fields = VKConstants.ALL_FIELDS - ) - ) - }?.response?.let { response -> - val dataConversationsMessages = response.items.map { item -> - item.conversation to item.lastMessage - } - val dataConversationsList = dataConversationsMessages.map { pair -> pair.first } - dataConversations.emit(dataConversationsList) - - val newProfiles = response.profiles - ?.map(BaseVkUser::mapToDomain) - ?.toMap(hashMapOf(), VkUser::id) ?: hashMapOf() - profiles.emit(newProfiles) - - val newGroups = response.groups - ?.map(BaseVkGroup::mapToDomain) - ?.toMap(hashMapOf(), VkGroup::id) ?: hashMapOf() - groups.emit(newGroups) - - val messages = dataConversationsMessages - .map { pair -> pair.second } - .mapNotNull { message -> message?.asVkMessage() } - .map { message -> - message.apply { - message.user = newProfiles[message.fromId] - message.group = newGroups[message.fromId] - message.actionUser = newProfiles[message.actionMemberId] - message.actionGroup = newGroups[message.actionMemberId] - } - } - - messagesRepository.store(messages) - - val photos = newProfiles.mapNotNull { profile -> profile.value.photo200 } + - newGroups.mapNotNull { group -> group.value.photo200 } - - photos.forEach { url -> - ImageRequest.Builder(AppGlobal.Instance) - .data(url) - .build() - .let(imageLoader::enqueue) - } - - val domainConversationsList = - dataConversationsList.mapToDomain().map { conversation -> - conversation.apply { - conversation.conversationUser = newProfiles[conversation.id] - conversation.conversationGroup = newGroups[conversation.id] - } - - } - emitConversations(domainConversationsList) - - isLoading.emitWithMain(false) - } - } - } - - private suspend fun emitConversations(conversations: List) = - withContext(Dispatchers.Default) { - domainConversations.emit(conversations) - } - - private suspend fun List.mapToDomain(): List = - this.map { baseConversation -> getFilledDomainVkConversation(baseConversation) } - - private suspend fun VkConversationDomain.fill(): VkConversationDomain { - val conversation = this - val messages = messagesRepository.getCached(conversation.id) - - val lastMessage = messages.find { it.id == conversation.lastMessageId } - - val userGroup = - VkUtils.getConversationUserGroup( - conversation, - profiles.value, - groups.value - ) - val actionUserGroup = - VkUtils.getMessageActionUserGroup( - lastMessage, - profiles.value, - groups.value - ) - val messageUserGroup = - VkUtils.getMessageUserGroup( - lastMessage, - profiles.value, - groups.value - ) - - conversation.conversationUser = userGroup.first - conversation.conversationGroup = userGroup.second - - val newMessage = lastMessage?.copy()?.apply { - this.user = messageUserGroup.first - this.group = messageUserGroup.second - this.actionUser = actionUserGroup.first - this.actionGroup = actionUserGroup.second - } - - conversation.lastMessage = newMessage - - return conversation - } - - private suspend fun getFilledDomainVkConversation( - baseConversation: BaseVkConversation, - defDomainConversation: VkConversationDomain? = null, - ): VkConversationDomain { - val conversation = defDomainConversation ?: baseConversation.mapToDomain() - return conversation.fill() - } - - private fun loadProfileUser() { - viewModelScope.launch(Dispatchers.IO) { - sendRequest { - usersRepository.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) - }?.response?.let { response -> - val users = response.map(BaseVkUser::mapToDomain) - usersRepository.storeUsers(users) - - UserConfig.vkUser.emit(users.first()) - } - } - } - - private fun deleteConversation(peerId: Int) { - viewModelScope.launch(Dispatchers.IO) { - sendRequest { - conversationsRepository.delete(ConversationsDeleteRequest(peerId)) - }?.let { - domainConversations.value.toMutableList().let { list -> - val index = list.indexOfFirst { conversation -> conversation.id == peerId } - if (index != -1) { - list.removeAt(index) - domainConversations.emit(list) - } - } - } - } - } - - private fun pinConversation(peerId: Int, pin: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - if (pin) { - sendRequest { - conversationsRepository.pin(ConversationsPinRequest(peerId)) - }?.let { handleConversationPinStateUpdate(peerId, true) } - } else { - sendRequest { - conversationsRepository.unpin(ConversationsUnpinRequest(peerId)) - }?.let { - handleConversationPinStateUpdate(peerId, false) - } - } - } - } - - // TODO: 07.01.2023, Danil Nikolaev: handle major AND minor id - private suspend fun handleConversationPinStateUpdate(peerId: Int, pin: Boolean) { - withContext(Dispatchers.IO) { - val conversationsList = domainConversations.value.toMutableList() - val conversationIndex = - conversationsList.findIndex { it.id == peerId } ?: return@withContext - - val conversation = conversationsList[conversationIndex].copy( - majorId = if (pin) (pinnedConversationsCount.value + 1) * 16 - else 0 - ).fill() - - conversationsList.removeAt(conversationIndex) - - if (pin) { - conversationsList.add(0, conversation) - } else { - conversationsList.add(pinnedConversationsCount.value - 1, conversation) - } - - emitConversations(conversationsList) - } - } - - private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { - viewModelScope.launch(Dispatchers.IO) { - val message = event.message - - messagesRepository.store(message) - - val newProfiles: HashMap = - (profiles.value + event.profiles) as HashMap - profiles.update { newProfiles } - - val newGroups: HashMap = - (groups.value + event.groups) as HashMap - groups.update { newGroups } - - val dataConversationsList = domainConversations.value.toMutableList() - val dataConversationIndex = dataConversationsList.findIndex { it.id == message.peerId } - - if (dataConversationIndex == null) { // диалога нет в списке - // pizdets - } else { - val dataConversation = dataConversationsList[dataConversationIndex] - var newConversation = dataConversation.copy( - lastMessageId = message.id, - lastConversationMessageId = -1 - ).fill().also { - it.lastMessage = message - } - if (!message.isOut) { - newConversation = newConversation.copy( - unreadCount = newConversation.unreadCount + 1 - ).fill().also { - it.lastMessage = message - } - } - - if (dataConversation.isPinned()) { - dataConversationsList[dataConversationIndex] = newConversation - emitConversations(dataConversationsList) - return@launch - } - - dataConversationsList.removeAt(dataConversationIndex) - - val toPosition = pinnedConversationsCount.value - dataConversationsList.add(toPosition, newConversation) - - emitConversations(dataConversationsList) - } - } - } - - private fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) { - viewModelScope.launch(Dispatchers.IO) { - val message = event.message - - messagesRepository.store(message) - - val conversationsList = domainConversations.value.toMutableList() - - val conversationIndex = conversationsList.findIndex { it.id == message.peerId } - if (conversationIndex == null) { // диалога нет в списке - - } else { - val conversation = conversationsList[conversationIndex] - conversationsList[conversationIndex] = conversation.copy( - lastMessageId = message.id, - lastConversationMessageId = -1 - ).fill().also { - it.lastMessage = message - } - - emitConversations(conversationsList) - } - } - } - - private fun handleReadIncomingMessage(event: LongPollEvent.VkMessageReadIncomingEvent) { - viewModelScope.launch(Dispatchers.IO) { - val conversationsList = domainConversations.value.toMutableList() - - val conversationIndex = - conversationsList.findIndex { it.id == event.peerId } ?: return@launch - - var conversation = conversationsList[conversationIndex] - conversation = conversation.copy( - inRead = event.messageId, - unreadCount = event.unreadCount - ).fill() - - conversationsList[conversationIndex] = conversation - - emitConversations(conversationsList) - } - } - - private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) = - viewModelScope.launch(Dispatchers.IO) { - val conversationsList = domainConversations.value.toMutableList() - - val conversationIndex = - conversationsList.findIndex { it.id == event.peerId } ?: return@launch - - var conversation = conversationsList[conversationIndex] - conversation = conversation.copy( - outRead = event.messageId, - unreadCount = event.unreadCount - ).fill() - - conversationsList[conversationIndex] = conversation - - emitConversations(conversationsList) - } - - // TODO: 07.01.2023, Danil Nikolaev: handle major AND minor id - private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) = - viewModelScope.launch(Dispatchers.IO) { - val conversationsList = domainConversations.value.toMutableList() - - val conversationIndex = - conversationsList.findIndex { it.id == event.peerId } ?: return@launch - - val pin = event.majorId > 0 - - var conversation = conversationsList[conversationIndex] - conversation = conversation.copy( - majorId = event.majorId - ).fill() - - conversationsList.removeAt(conversationIndex) - - if (pin) { - conversationsList.add(0, conversation) - } else { - conversationsList.add(pinnedConversationsCount.value - 1, conversation) - } - - emitConversations(conversationsList) - } - - - private fun openMessagesHistoryScreen( - conversationUi: VkConversationUi, - user: VkUser?, - group: VkGroup?, - ) { - val conversation = domainConversations.value.find { domainConversation -> - domainConversation.id == conversationUi.id - } ?: return - - router.navigateTo(Screens.MessagesHistory(conversation, user, group)) - } - - private fun readConversation(conversationId: Int, startMessageId: Int) { - viewModelScope.launch(Dispatchers.IO) { - sendRequest { - messagesRepository.markAsRead( - peerId = conversationId, - startMessageId = startMessageId - ) - }?.response?.let { messageId -> - domainConversations.value.toMutableList().let { list -> - val index = list.findIndex { it.id == conversationId } ?: return@launch - val newConversation = list[index].copy(inRead = messageId) - list[index] = newConversation - - domainConversations.emit(list) - } - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/adapter/ConversationsDelegates.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/adapter/ConversationsDelegates.kt deleted file mode 100644 index 793eb36b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/adapter/ConversationsDelegates.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.meloda.fast.screens.conversations.adapter - -import android.graphics.drawable.Drawable -import android.text.Spanned -import android.text.style.ForegroundColorSpan -import coil.ImageLoader -import coil.request.ImageRequest -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -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.presentation.VkConversationUi -import com.meloda.fast.base.adapter.OnItemClickListener -import com.meloda.fast.base.adapter.OnItemLongClickListener -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.databinding.ItemConversationBinding -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.isFalse -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.visible -import com.meloda.fast.model.base.AdapterDiffItem -import com.meloda.fast.model.base.UiImage -import com.meloda.fast.model.base.parseString -import com.meloda.fast.model.base.setImage -import com.meloda.fast.screens.conversations.ConversationsResourceProvider -import com.meloda.fast.screens.settings.SettingsFragment - -fun conversationDelegate( - onItemClickListener: OnItemClickListener, - onItemLongClickListener: OnItemLongClickListener, -) = adapterDelegateViewBinding( - viewBinding = { layoutInflater, parent -> - ItemConversationBinding.inflate(layoutInflater, parent, false) - } -) { - binding.root.setOnClickListener { onItemClickListener.onItemClick(item) } - binding.root.setOnLongClickListener { onItemLongClickListener.onLongItemClick(item) } - - val imageLoader = ImageLoader.Builder(context) - .crossfade(true) - .build() - - val resourceProvider = ConversationsResourceProvider(context) - - bind { - val isMultilineEnabled = - AppGlobal.preferences.getBoolean(SettingsFragment.KEY_APPEARANCE_MULTILINE, true) - val maxLines = if (isMultilineEnabled) 2 else 1 - - binding.title.maxLines = maxLines - binding.message.maxLines = maxLines - - binding.container.background = - if (item.isUnread) resourceProvider.conversationUnreadBackground else null - - binding.title.text = item.title.parseString(context) - - binding.date.text = item.date - - binding.service.toggleVisibility(item.actionState != ActionState.None) - binding.phantomIcon.toggleVisibility(item.actionState == ActionState.Phantom) - binding.callIcon.toggleVisibility(item.actionState == ActionState.CallInProgress) - - binding.counter.toggleVisibility(item.unreadCount != null) - binding.counter.text = item.unreadCount - - binding.textAttachment.toggleVisibility(item.attachmentImage != null) - - binding.pin.toggleVisibility(item.isPinned) - - binding.online.toggleVisibility(item.isOnline) - - binding.avatarPlaceholder.visible() - - (item.avatar as? UiImage.Url)?.let { image -> - ImageRequest.Builder(context) - .data(image.url) - .target( - onSuccess = { result -> - binding.avatar.setImageDrawable(result) - binding.avatarPlaceholder.gone() - } - ) - .build().let(imageLoader::enqueue) - } ?: { - binding.avatar.setImage(item.avatar) { - asCircle = true - crossFade = true - onLoadedAction = { binding.avatarPlaceholder.gone() } - } - } - - val actionMessage = VkUtils.getActionConversationText( - context = context, - message = item.lastMessage, - youPrefix = resourceProvider.youPrefix, - messageUser = item.lastMessage?.user, - messageGroup = item.lastMessage?.group, - action = item.lastMessage?.getPreparedAction(), - actionUser = item.lastMessage?.actionUser, - actionGroup = item.lastMessage?.actionGroup - ) - - val attachmentIcon: Drawable? = when { - item.lastMessage?.text == null -> null - !item.lastMessage?.forwards.isNullOrEmpty() -> { - if (item.lastMessage?.forwards?.size == 1) { - resourceProvider.iconForwardedMessage - } else { - resourceProvider.iconForwardedMessages - } - } - else -> VkUtils.getAttachmentConversationIcon(context, item.lastMessage) - } - - binding.textAttachment.toggleVisibility(attachmentIcon != null) - binding.textAttachment.setImageDrawable(attachmentIcon) - - val attachmentText = (if (attachmentIcon == null) VkUtils.getAttachmentText( - message = item.lastMessage - ) else null)?.parseString(context) - - val forwardsMessage = (if (item.lastMessage?.text == null) VkUtils.getForwardsText( - message = item.lastMessage - ) else null)?.parseString(context) - - val messageText = (if ( - actionMessage != null || - forwardsMessage != null || - attachmentText != null - ) "" - else item.lastMessage?.text ?: "").run { VkUtils.prepareMessageText(this) } - - val coloredMessage = actionMessage ?: attachmentText ?: forwardsMessage ?: "" - - var prefix = when { - actionMessage != null -> "" - item.lastMessage?.isOut.isTrue -> "${resourceProvider.youPrefix}: " - else -> - when { - item.lastMessage?.isUser().isTrue && item.lastMessage?.user != null && item.lastMessage?.user?.firstName?.isNotBlank().isTrue -> { - "${item.lastMessage?.user?.firstName}: " - } - item.lastMessage?.isGroup().isTrue && item.lastMessage?.group != null && item.lastMessage?.group?.name?.isNotBlank().isTrue -> { - "${item.lastMessage?.group?.name}: " - } - else -> "" - } - } - - if ((!item.peerType.isChat() && item.lastMessage?.isOut.isFalse) || item.conversationId == UserConfig.userId) - prefix = "" - - val spanText = "$prefix$coloredMessage$messageText" - - val visualizedMessageText = VkUtils.visualizeMentions( - messageText = spanText, - resourceProvider.colorPrimary - ) - - val length = prefix.length + coloredMessage.length - visualizedMessageText.setSpan( - ForegroundColorSpan(resourceProvider.colorOutline), - 0, - length, - if (length > 0) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE else 0 - ) - - binding.message.text = visualizedMessageText - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/di/ConversationsModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/di/ConversationsModule.kt deleted file mode 100644 index aebacc6c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/di/ConversationsModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.conversations.di - -import com.meloda.fast.screens.conversations.ConversationsViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val conversationsModule = module { - viewModelOf(::ConversationsViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt deleted file mode 100644 index 5e74ac3e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ /dev/null @@ -1,419 +0,0 @@ -package com.meloda.fast.screens.login - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import androidx.compose.animation.* -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.* -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.base.viewmodel.ViewModelUtils -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.databinding.DialogFastLoginBinding -import com.meloda.fast.ext.* -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.login.model.LoginScreenState -import com.meloda.fast.ui.AppTheme -import com.meloda.fast.ui.widgets.TextFieldErrorText -import org.koin.androidx.viewmodel.ext.android.viewModel - - -class LoginFragment : BaseFragment() { - - private val viewModel: LoginViewModel by viewModel() - - private val backPressedCallback = object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() { - viewModel.onBackPressed() - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - activity?.onBackPressedDispatcher?.addCallback(backPressedCallback) - - viewModel.isNeedToShowLogo.listenValue { needToShow -> - backPressedCallback.isEnabled = !needToShow - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - listenViewModel() - - (view as? ComposeView)?.apply { - setContent { - val showLogo by viewModel.isNeedToShowLogo.collectAsState() - - AppTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - ) { - if (showLogo) { - LoginLogo() - } else { - val state by viewModel.screenState.collectAsStateWithLifecycle() - - LoginSignIn( - onSignInClick = viewModel::onSignInButtonClicked, - onLoginInputChanged = viewModel::onLoginInputChanged, - onPasswordInputChanged = viewModel::onPasswordInputChanged, - onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked, - state = state, - ) - } - } - } - } - } - } - - private fun listenViewModel() = with(viewModel) { - events.listenValue(::handleEvent) - isNeedToShowErrorDialog.listenValue(::handleErrorAlertShow) - isNeedToShowFastLoginDialog.listenValue(::handleFastLoginAlertShow) - } - - private fun handleEvent(event: VkEvent) { - ViewModelUtils.parseEvent(this, event) - } - - private fun handleErrorAlertShow(isNeedToShow: Boolean) { - if (isNeedToShow) { - showErrorDialog() - } - } - - private fun handleFastLoginAlertShow(isNeedToShow: Boolean) { - if (isNeedToShow) { - showFastLoginDialog() - } - } - - private fun showErrorDialog() { - context?.showDialog( - title = UiText.Resource(R.string.title_error), - message = UiText.Simple(viewModel.screenState.value.error.orEmpty()), - positiveText = UiText.Resource(R.string.ok), - onDismissAction = viewModel::onErrorDialogDismissed - ) - } - - private fun showFastLoginDialog() { - val dialogFastLoginBinding = DialogFastLoginBinding.inflate(layoutInflater, null, false) - - context?.showDialog( - title = UiText.Resource(R.string.fast_login_title), - view = dialogFastLoginBinding.root, - positiveText = UiText.Resource(R.string.ok), - positiveAction = { - val text = dialogFastLoginBinding.fastLoginText.trimmedText - if (text.isEmpty()) return@showDialog - - val split = text.split(";") - try { - val login = split[0] - val password = split[1] - - viewModel.onLoginInputChanged(login) - viewModel.onPasswordInputChanged(password) - - viewModel.onFastLoginDialogOkButtonClicked() - } catch (e: Exception) { - e.printStackTrace() - } - }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onFastLoginDialogDismissed - ) - } - - @OptIn(ExperimentalFoundationApi::class) - @Composable - fun LoginLogo() { - Box( - modifier = Modifier - .fillMaxSize() - .padding(30.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(id = R.drawable.ic_logo_big), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), - modifier = Modifier.combinedClickableSound( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onLongClick = viewModel::onLogoLongClicked - ) - ) - Spacer(modifier = Modifier.height(46.dp)) - Text( - text = "Fast Messenger", - style = MaterialTheme.typography.displayMedium, - color = MaterialTheme.colorScheme.onBackground - ) - } - - FloatingActionButton( - onClick = viewModel::onLogoNextButtonClicked, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.align(Alignment.BottomCenter) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_end), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } - } - - @Preview - @Composable - fun LoginSignInPreview() { - AppTheme( - useDarkTheme = false, - useDynamicColors = false - ) { - Surface(color = MaterialTheme.colorScheme.background) { - LoginSignIn( - state = LoginScreenState.EMPTY, - onSignInClick = { }, - onLoginInputChanged = {}, - onPasswordInputChanged = {}, - onPasswordVisibilityButtonClicked = {} - ) - } - } - } - - @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) - @Composable - fun LoginSignIn( - onSignInClick: () -> Unit, - onLoginInputChanged: (String) -> Unit, - onPasswordInputChanged: (String) -> Unit, - onPasswordVisibilityButtonClicked: () -> Unit, - state: LoginScreenState - ) { - val focusManager = LocalFocusManager.current - val (loginFocusable, passwordFocusable) = FocusRequester.createRefs() - val isLoading = state.isLoading - - val goButtonClickAction = { - if (!isLoading) { - focusManager.clearFocus() - onSignInClick.invoke() - } - } - val loginFieldTabClick = { - passwordFocusable.requestFocus() - true - } - - Box( - modifier = Modifier - .fillMaxSize() - .padding(30.dp) - .imePadding() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center) - ) { - Text( - text = "Sign in to VK", - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.displayMedium - ) - - Spacer(modifier = Modifier.height(58.dp)) - - var loginText by remember { mutableStateOf(TextFieldValue(state.login)) } - val showLoginError = state.loginError - - TextField( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .handleEnterKey(loginFieldTabClick::invoke) - .handleTabKey(loginFieldTabClick::invoke) - .focusRequester(loginFocusable), - value = loginText, - onValueChange = { newText -> - loginText = newText - onLoginInputChanged.invoke(newText.text) - }, - label = { Text(text = "Login") }, - placeholder = { Text(text = "Login") }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_round_person_24), - contentDescription = null, - tint = if (showLoginError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - } - ) - }, - shape = RoundedCornerShape(10.dp), - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Email - ), - keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }), - isError = showLoginError, - singleLine = true - ) - AnimatedVisibility(visible = showLoginError) { - TextFieldErrorText(text = "Field must not be empty") - } - - Spacer(modifier = Modifier.height(16.dp)) - - var passwordText by remember { mutableStateOf(TextFieldValue(state.password)) } - val showPasswordError = state.passwordError - var passwordVisible = state.passwordVisible - - TextField( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .handleEnterKey { - goButtonClickAction.invoke() - true - } - .focusRequester(passwordFocusable), - value = passwordText, - onValueChange = { newText -> - passwordText = newText - onPasswordInputChanged.invoke(newText.text) - }, - label = { Text(text = "Password") }, - placeholder = { Text(text = "Password") }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.round_vpn_key_24), - contentDescription = null, - tint = if (showPasswordError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - } - ) - }, - trailingIcon = { - val imagePainter = painterResource( - id = if (passwordVisible) R.drawable.round_visibility_off_24 - else R.drawable.round_visibility_24 - ) - - IconButton( - onClick = { - onPasswordVisibilityButtonClicked.invoke() - passwordVisible = !passwordVisible - } - ) { - Icon(painter = imagePainter, contentDescription = null) - } - }, - shape = RoundedCornerShape(10.dp), - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Go, - keyboardType = KeyboardType.Password - ), - keyboardActions = KeyboardActions( - onGo = { goButtonClickAction.invoke() } - ), - isError = showPasswordError, - visualTransformation = if (passwordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - singleLine = true - ) - AnimatedVisibility(visible = showPasswordError) { - TextFieldErrorText(text = "Field must not be empty") - } - } - - Box( - modifier = Modifier.align(Alignment.BottomCenter), - contentAlignment = Alignment.Center - ) { - - FloatingActionButton( - onClick = goButtonClickAction::invoke, - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_end), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - AnimatedVisibility( - visible = isLoading, - enter = fadeIn(), - exit = fadeOut() - ) { - CircularProgressIndicator() - } - } - } - } - - companion object { - - fun newInstance(): LoginFragment { - return LoginFragment() - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginScreens.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginScreens.kt deleted file mode 100644 index 7b379af1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginScreens.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.meloda.fast.screens.login - -import com.github.terrakok.cicerone.androidx.FragmentScreen - -object LoginScreens { - - fun login() = FragmentScreen { - LoginFragment.newInstance() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt deleted file mode 100644 index 5527d5a9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ /dev/null @@ -1,370 +0,0 @@ -package com.meloda.fast.screens.login - -import androidx.lifecycle.viewModelScope -import com.github.terrakok.cicerone.Router -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.network.WrongTwoFaCodeError -import com.meloda.fast.api.network.WrongTwoFaCodeFormatError -import com.meloda.fast.api.network.auth.AuthDirectRequest -import com.meloda.fast.base.viewmodel.CaptchaRequiredEvent -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.base.viewmodel.UnknownErrorEvent -import com.meloda.fast.base.viewmodel.ValidationRequiredEvent -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.common.Screens -import com.meloda.fast.data.account.AccountsDao -import com.meloda.fast.data.auth.AuthRepository -import com.meloda.fast.ext.emitOnMainScope -import com.meloda.fast.ext.emitOnScope -import com.meloda.fast.ext.listenValue -import com.meloda.fast.ext.updateValue -import com.meloda.fast.model.AppAccount -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.captcha.screen.CaptchaArguments -import com.meloda.fast.screens.captcha.screen.CaptchaResult -import com.meloda.fast.screens.captcha.screen.CaptchaScreen -import com.meloda.fast.screens.login.model.LoginScreenState -import com.meloda.fast.screens.login.model.LoginValidationResult -import com.meloda.fast.screens.login.validation.LoginValidator -import com.meloda.fast.screens.twofa.model.TwoFaArguments -import com.meloda.fast.screens.twofa.model.TwoFaResult -import com.meloda.fast.screens.twofa.model.TwoFaValidationType -import com.meloda.fast.screens.twofa.screen.TwoFaScreen -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -interface LoginViewModel { - val events: Flow - - val isNeedToShowLogo: StateFlow - - val screenState: StateFlow - - val isNeedToShowFastLoginDialog: Flow - val isNeedToShowErrorDialog: Flow - - fun onBackPressed() - - fun onPasswordVisibilityButtonClicked() - - fun onLogoNextButtonClicked() - - fun onLoginInputChanged(newLogin: String) - fun onPasswordInputChanged(newPassword: String) - - fun onSignInButtonClicked() - fun onSignInButtonLongClicked() - - fun onFastLoginDialogOkButtonClicked() - - fun onFastLoginDialogDismissed() - fun onErrorDialogDismissed() - fun onLogoLongClicked() -} - -class LoginViewModelImpl constructor( - private val authRepository: AuthRepository, - private val router: Router, - private val accounts: AccountsDao, - private val loginValidator: LoginValidator, - private val captchaScreen: CaptchaScreen, - private val twoFaScreen: TwoFaScreen -) : DeprecatedBaseViewModel(), LoginViewModel { - - override val isNeedToShowLogo = MutableStateFlow(true) - - override val screenState = MutableStateFlow(LoginScreenState.EMPTY) - - private val validationState: StateFlow> = - screenState.map(loginValidator::validate) - .stateIn(viewModelScope, SharingStarted.Eagerly, listOf(LoginValidationResult.Empty)) - - private val captchaResult = captchaScreen.resultFlow - private val twoFaResult = twoFaScreen.resultFlow - - override val events = MutableSharedFlow( - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - override val isNeedToShowErrorDialog = MutableStateFlow(false) - override val isNeedToShowFastLoginDialog = MutableStateFlow(false) - - private var currentValidationEvent: ValidationRequiredEvent? = null - - init { - tasksEvent.listenValue(::handleEvent) - - captchaResult.listenValue { result -> - when (result) { - is CaptchaResult.Success -> { - val sid = result.sid - val code = result.code - val newState = screenState.value.copy( - captchaSid = sid, captchaCode = code - ) - screenState.updateValue(newState) - - login() - } - - else -> Unit - } - } - - twoFaResult.listenValue { result -> - when (result) { - is TwoFaResult.Success -> { - val sid = result.sid - val code = result.code - val newState = screenState.value.copy( - validationSid = sid, validationCode = code - ) - screenState.updateValue(newState) - - login() - } - - else -> Unit - } - } - } - - private fun handleEvent(event: VkEvent) { - when (event) { - is CaptchaRequiredEvent -> onCaptchaEventReceived(event) - is ValidationRequiredEvent -> onValidationEventReceived(event) - else -> events.emitOnScope(event) - } - } - - override fun onBackPressed() { - if (isNeedToShowLogo.value) { - router.exit() - } else { - isNeedToShowLogo.updateValue(true) - } - } - - override fun onPasswordVisibilityButtonClicked() { - val newState = screenState.value.copy( - passwordVisible = !screenState.value.passwordVisible - ) - screenState.updateValue(newState) - } - - override fun onLogoNextButtonClicked() { - isNeedToShowLogo.emitOnMainScope(false) - } - - override fun onLoginInputChanged(newLogin: String) { - val newState = screenState.value.copy( - login = newLogin.trim(), - loginError = false - ) - screenState.updateValue(newState) - } - - override fun onPasswordInputChanged(newPassword: String) { - val newState = screenState.value.copy( - password = newPassword.trim(), - passwordError = false - ) - screenState.updateValue(newState) - } - - private fun onCaptchaEventReceived(event: CaptchaRequiredEvent) { - val captchaSid = event.sid - val captchaImage = event.image - - val newState = screenState.value.copy( - captchaSid = captchaSid, - captchaImage = captchaImage - ) - screenState.update { newState } - - showCaptchaScreen( - CaptchaArguments( - captchaSid = captchaSid, - captchaImage = captchaImage - ) - ) - } - - private fun showCaptchaScreen(args: CaptchaArguments) { - captchaScreen.show(router, args) - } - - private fun onValidationEventReceived(event: ValidationRequiredEvent) { - currentValidationEvent = event - - val validationSid = event.sid - val newForm = screenState.value.copy( - validationSid = validationSid - ) - screenState.update { newForm } - - showValidationScreen( - TwoFaArguments( - validationSid = event.sid, - redirectUri = event.redirectUri, - phoneMask = event.phoneMask, - validationType = TwoFaValidationType.parse(event.validationType), - canResendSms = event.canResendSms, - wrongCodeError = event.codeError - ) - ) - } - - private fun showValidationScreen(args: TwoFaArguments) { - twoFaScreen.show(router, args) - } - - override fun onSignInButtonClicked() { - login() - } - - override fun onSignInButtonLongClicked() { - isNeedToShowFastLoginDialog.emitOnMainScope(true) - } - - override fun onFastLoginDialogOkButtonClicked() { - login() - } - - override fun onFastLoginDialogDismissed() { - isNeedToShowFastLoginDialog.emitOnMainScope(false) - } - - override fun onErrorDialogDismissed() { - isNeedToShowErrorDialog.emitOnMainScope(false) - } - - override fun onLogoLongClicked() { - router.navigateTo(Screens.Settings()) - } - - private fun login(forceSms: Boolean = false) { - currentValidationEvent?.let { event -> - if (!screenState.value.validationSid.isNullOrBlank() && screenState.value.validationCode == null) { - handleEvent(event) - return - } - } - - val state = screenState.value.copy() - - val clearedState = screenState.value.copy( - captchaSid = null, - captchaImage = null, - captchaCode = null, - validationSid = null, - validationCode = null - ) - - screenState.update { clearedState } - - processValidation() - if (!validationState.value.contains(LoginValidationResult.Valid)) return - - viewModelScope.launch(Dispatchers.IO) { - var newState = screenState.value.copy( - isLoading = true - ) - screenState.update { newState } - - sendRequest( - onError = { error -> - when (error) { - is WrongTwoFaCodeError, WrongTwoFaCodeFormatError -> { - currentValidationEvent?.let { event -> - val codeError = UiText.Simple( - if (error is WrongTwoFaCodeError) "Wrong code" - else "Wrong code format" - ) - handleEvent(event.copy(codeError = codeError)) - true - } ?: false - } - - else -> false - } - }, - request = { - val requestModel = AuthDirectRequest( - grantType = VKConstants.Auth.GrantType.PASSWORD, - clientId = VKConstants.VK_APP_ID, - clientSecret = VKConstants.VK_SECRET, - username = state.login, - password = state.password, - scope = VKConstants.Auth.SCOPE, - twoFaForceSms = forceSms, - twoFaCode = state.validationCode, - captchaSid = state.captchaSid, - captchaKey = state.captchaCode - ) - - authRepository.auth(requestModel) - } - )?.let { response -> - val userId = response.userId - val accessToken = response.accessToken - - if (userId == null || accessToken == null) { - sendEvent(UnknownErrorEvent) - return@let - } - - if (currentValidationEvent != null) { - currentValidationEvent = null - } - - val currentAccount = AppAccount( - userId = userId, - accessToken = accessToken, - fastToken = null - ).also { account -> - UserConfig.currentUserId = account.userId - UserConfig.userId = account.userId - UserConfig.accessToken = account.accessToken - UserConfig.fastToken = account.fastToken - } - - accounts.insert(listOf(currentAccount)) - - router.replaceScreen(Screens.Main()) - } - - newState = screenState.value.copy( - isLoading = false - ) - screenState.update { newState } - } - } - - private fun processValidation() { - validationState.value.forEach { result -> - when (result) { - LoginValidationResult.LoginEmpty -> { - screenState.updateValue(screenState.value.copy(loginError = true)) - } - - LoginValidationResult.PasswordEmpty -> { - screenState.updateValue(screenState.value.copy(passwordError = true)) - } - - LoginValidationResult.Empty -> Unit - LoginValidationResult.Valid -> Unit - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/di/LoginModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/di/LoginModule.kt deleted file mode 100644 index a7e4c4c0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/di/LoginModule.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.screens.login.di - -import com.meloda.fast.screens.login.LoginViewModelImpl -import com.meloda.fast.screens.login.validation.LoginValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val loginModule = module { - single { LoginValidator() } - viewModelOf(::LoginViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginResult.kt deleted file mode 100644 index 03f3090d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.screens.login.model - -sealed class LoginResult { - object Authorized : LoginResult() - object Cancelled : LoginResult() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginValidationResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginValidationResult.kt deleted file mode 100644 index 923ef6b7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginValidationResult.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.screens.login.model - -sealed class LoginValidationResult { - - object LoginEmpty : LoginValidationResult() - - object PasswordEmpty : LoginValidationResult() - - object Empty : LoginValidationResult() - - object Valid : LoginValidationResult() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/screen/LoginScreen.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/screen/LoginScreen.kt deleted file mode 100644 index d49612d2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/screen/LoginScreen.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.meloda.fast.screens.login.screen - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.base.screen.AppScreen -import com.meloda.fast.base.screen.createResultFlow -import com.meloda.fast.screens.login.model.LoginResult - -class LoginScreen : AppScreen { - - override val resultFlow = createResultFlow() - override var args: Unit = Unit - - override fun show(router: Router, args: Unit) { - this.args = args - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt deleted file mode 100644 index bf48d0d6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.meloda.fast.screens.main - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.fragment.app.setFragmentResult -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.base.viewmodel.ViewModelUtils -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.ext.listenValue -import com.meloda.fast.screens.main.activity.ServicesState -import org.koin.androidx.viewmodel.ext.android.viewModel - -class MainFragment : BaseFragment() { - - private val viewModel: MainViewModel by viewModel() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - Log.d("MainFragment", "onCreate: viewModel: $viewModel") - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = View(context) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - listenViewModel() - } - - private fun listenViewModel() { - viewModel.events.listenValue(::onEvent) - - viewModel.servicesState.listenValue { state -> - val enableServices = state == ServicesState.Started - setFragmentResult( - START_SERVICES_KEY, - bundleOf(START_SERVICES_ARG_ENABLE to enableServices) - ) - } - } - - private fun onEvent(event: VkEvent) { - ViewModelUtils.parseEvent(this, event) - } - - companion object { - const val START_SERVICES_KEY = "start_services" - const val START_SERVICES_ARG_ENABLE = "enable" - - fun newInstance(): MainFragment = MainFragment() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt deleted file mode 100644 index d34e1391..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.meloda.fast.screens.main - -import android.util.Log -import androidx.lifecycle.viewModelScope -import com.github.terrakok.cicerone.Router -import com.github.terrakok.cicerone.Screen -import com.meloda.fast.api.UserConfig -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.common.Screens -import com.meloda.fast.screens.main.activity.ServicesState -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch - -interface MainViewModel { - val events: Flow - - val servicesState: Flow -} - -class MainViewModelImpl constructor( - private val router: Router -) : MainViewModel, DeprecatedBaseViewModel() { - - override val events = tasksEvent.map { it } - - override val servicesState = MutableStateFlow(ServicesState.Unknown) - - init { - checkSession() - } - - private fun checkSession() { - viewModelScope.launch { - val currentUserId = UserConfig.currentUserId - val userId = UserConfig.userId - val accessToken = UserConfig.accessToken - val fastToken = UserConfig.fastToken - - Log.d( - "MainViewModel", - "checkSession: currentUserId: $currentUserId; userId: $userId; accessToken: $accessToken; fastToken: $fastToken" - ) - - if (UserConfig.isLoggedIn()) { - servicesState.emit(ServicesState.Started) - openScreen(Screens.Conversations()) - } else { - servicesState.emit(ServicesState.Stopped) - openScreen(Screens.Login()) - } - } - } - - private fun openScreen(screen: Screen) { - router.replaceScreen(screen) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollState.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollState.kt deleted file mode 100644 index 0e0ebf16..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.screens.main.activity - -sealed class LongPollState { - object ForegroundService : LongPollState() - object DefaultService : LongPollState() - object Stop : LongPollState() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollUtils.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollUtils.kt deleted file mode 100644 index d0078dcb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollUtils.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.meloda.fast.screens.main.activity - -import android.Manifest -import android.content.Intent -import android.net.Uri -import android.provider.Settings -import androidx.core.content.edit -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.lifecycleScope -import com.fondesa.kpermissions.coroutines.sendSuspend -import com.fondesa.kpermissions.extension.permissionsBuilder -import com.fondesa.kpermissions.isGranted -import com.fondesa.kpermissions.isPermanentlyDenied -import com.meloda.fast.R -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.sdk33AndUp -import com.meloda.fast.ext.showDialog -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.settings.SettingsFragment -import kotlinx.coroutines.launch - -object LongPollUtils { - - fun requestNotificationsPermission( - fragmentActivity: FragmentActivity, - onStateChangedAction: (LongPollState) -> Unit, - fromSettings: Boolean = false, - ) { - val longPollInForegroundEnabled = - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - SettingsFragment.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND - ) - - sdk33AndUp { - fragmentActivity.lifecycleScope.launch { - val result = - fragmentActivity.permissionsBuilder(Manifest.permission.POST_NOTIFICATIONS) - .build() - .sendSuspend() - .first() - - val resultToEmit: LongPollState = when { - longPollInForegroundEnabled && result.isGranted() -> LongPollState.ForegroundService - else -> LongPollState.DefaultService - } - - onStateChangedAction.invoke(resultToEmit) - - val isLongPollOnlyInsideApp = - AppGlobal.preferences.getBoolean("lp_inside_app", false) - - if (result.isGranted()) { - AppGlobal.preferences.edit { putBoolean("lp_inside_app", false) } - } - - if (longPollInForegroundEnabled && - !result.isGranted() && - (!isLongPollOnlyInsideApp || fromSettings) - ) { - showNotificationsPermissionAlert( - fragmentActivity, - onStateChangedAction, - result.isPermanentlyDenied(), - ) - } - } - } ?: run { - onStateChangedAction.invoke( - if (longPollInForegroundEnabled) LongPollState.ForegroundService - else LongPollState.DefaultService - ) - } - } - - private fun showNotificationsPermissionAlert( - fragmentActivity: FragmentActivity, - onStateChangedAction: (LongPollState) -> Unit, - permanentlyDenied: Boolean, - ) { - val positiveText = - UiText.Simple(if (permanentlyDenied) "Open settings" else "Grant") - val positiveAction = { - if (permanentlyDenied) { - val intent = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse("package:${fragmentActivity.packageName}") - ) - intent.addCategory(Intent.CATEGORY_DEFAULT) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - try { - fragmentActivity.startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - } - } else { - requestNotificationsPermission(fragmentActivity, onStateChangedAction) - } - } - - val neutralText = - if (permanentlyDenied) UiText.Resource(R.string.ok) - else UiText.Simple("Dismiss") - val neutralAction = { - if (permanentlyDenied) { - AppGlobal.preferences.edit { - putBoolean("lp_inside_app", true) - putBoolean(SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, false) - } - } else Unit - } - - fragmentActivity.showDialog( - title = UiText.Resource(R.string.warning), - message = UiText.Simple( - "You denied notifications permission." + - "\nWithout notifications LongPoll service will work only inside app." + - "\nThis means that messages will only be updated while app is on the screen" - ), - positiveText = positiveText, - positiveAction = positiveAction, - neutralText = neutralText, - neutralAction = neutralAction, - isCancelable = false - ) - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/MainActivity.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/activity/MainActivity.kt deleted file mode 100644 index 2fcda73b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/MainActivity.kt +++ /dev/null @@ -1,316 +0,0 @@ -package com.meloda.fast.screens.main.activity - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.core.content.edit -import androidx.lifecycle.lifecycleScope -import com.github.terrakok.cicerone.NavigatorHolder -import com.github.terrakok.cicerone.Router -import com.github.terrakok.cicerone.androidx.AppNavigator -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.meloda.fast.BuildConfig -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.base.BaseActivity -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.Screens -import com.meloda.fast.data.account.AccountsDao -import com.meloda.fast.ext.edgeToEdge -import com.meloda.fast.ext.isSdkAtLeast -import com.meloda.fast.ext.listenValue -import com.meloda.fast.screens.main.MainFragment -import com.meloda.fast.screens.main.activity.LongPollUtils.requestNotificationsPermission -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.service.LongPollService -import com.meloda.fast.service.OnlineService -import com.meloda.fast.util.AndroidUtils -import com.microsoft.appcenter.AppCenter -import com.microsoft.appcenter.analytics.Analytics -import com.microsoft.appcenter.crashes.Crashes -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject - -class MainActivity : BaseActivity(R.layout.activity_main) { - - private val navigator = object : AppNavigator(this, R.id.root_fragment_container) {} - - private val navigatorHolder: NavigatorHolder by inject() - - private val router: Router by inject() - - private val accountsDao: AccountsDao by inject() - - private val updatesParser: LongPollUpdatesParser by inject() - - private var isOnlineServiceWasLaunched: Boolean = false - - private var savedInstanceState: Bundle? = null - - override fun onResumeFragments() { - navigatorHolder.setNavigator(navigator) - super.onResumeFragments() - } - - override fun onPause() { - if (isOnlineServiceWasLaunched) { - toggleOnlineService(false) - } - navigatorHolder.removeNavigator() - super.onPause() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - this.savedInstanceState = savedInstanceState - edgeToEdge() - - createNotificationChannels() - - AppCenter.configure(application, BuildConfig.msAppCenterAppToken) - - if (!BuildConfig.DEBUG) { - AppCenter.start(Analytics::class.java) - } - - val enableCrashLogs = - AppGlobal.preferences.getBoolean(SettingsFragment.KEY_MS_APPCENTER_ENABLE, true) - || (BuildConfig.DEBUG && AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_MS_APPCENTER_ENABLE_ON_DEBUG, - false - )) - - if (enableCrashLogs) { - AppCenter.start(Crashes::class.java) - } - - if (UserConfig.currentUserId == -1) { - openMainScreen() - } else { - initUserConfig() - } - - // TODO: 09.04.2023, Danil Nikolaev: implement checking updates on startup - - // TODO: 09.04.2023, Danil Nikolaev: rewrite this - supportFragmentManager.setFragmentResultListener( - MainFragment.START_SERVICES_KEY, - this - ) { _, result -> - val enable = result.getBoolean(MainFragment.START_SERVICES_ARG_ENABLE, true) - if (enable) { - requestNotificationsPermission( - fragmentActivity = this, - onStateChangedAction = { state -> - lifecycleScope.launch { longPollState.emit(state) } - } - ) - - startServices() - } else { - stopServices() - } - } - - // TODO: 09.04.2023, Danil Nikolaev: rewrite this - longPollState.listenValue { state -> - stopLongPollService() - - when (state) { - LongPollState.DefaultService -> startLongPollService(false) - LongPollState.ForegroundService -> startLongPollService(true) - else -> Unit - } - } - } - - 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)) - } - } - - override fun onResume() { - super.onResume() - - if (isOnlineServiceWasLaunched) { - toggleOnlineService(true) - } - - Crashes.getLastSessionCrashReport().thenAccept { report -> - if (report != null) { - if (AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_DEBUG_SHOW_CRASH_ALERT, - true - ) - ) { - val stackTrace = report.stackTrace - - MaterialAlertDialogBuilder(this) - .setTitle(R.string.app_crash_occurred) - .setMessage("Stacktrace: $stackTrace") - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.copy) { _, _ -> - AndroidUtils.copyText( - label = "Fast_Crash_Report", - text = stackTrace - ) - Toast.makeText(this, "Copied", Toast.LENGTH_SHORT).show() - } - .setNeutralButton(R.string.share) { _, _ -> - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, stackTrace) - type = "text/plain" - } - - val shareIntent = Intent.createChooser(sendIntent, "Share stacktrace") - try { - startActivity(shareIntent) - } catch (e: Exception) { - e.printStackTrace() - - runOnUiThread { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.warning) - .setMessage("Can't share") - .setPositiveButton(R.string.ok, null) - .show() - } - } - } - .show() - } - } - } - - if (AppGlobal.preferences.getBoolean(LongPollService.KeyLongPollWasDestroyed, false)) { - AppGlobal.preferences.edit { - putBoolean(LongPollService.KeyLongPollWasDestroyed, false) - } - - startServices() - } - } - - private fun startServices() { - toggleOnlineService(true) - } - - private fun stopServices() { - toggleOnlineService(false) - } - - private fun createLongPollIntent(asForeground: Boolean? = null): Intent = - Intent(this, LongPollService::class.java).apply { - asForeground?.let { putExtra("foreground", it) } - } - - private fun startLongPollService(asForeground: Boolean) { - val longPollIntent = createLongPollIntent(asForeground) - - if (asForeground) { - ContextCompat.startForegroundService(this, longPollIntent) - } else { - startService(longPollIntent) - } - } - - private fun stopLongPollService() { - stopService(createLongPollIntent()) - } - - private fun toggleOnlineService(enable: Boolean) { - if (enable) { - isOnlineServiceWasLaunched = true - startService(Intent(this, OnlineService::class.java)) - } else { - stopService(Intent(this, OnlineService::class.java)) - } - } - - private fun initUserConfig() { - if (UserConfig.currentUserId == -1) return - - lifecycleScope.launch { - val accounts = accountsDao.getAll() - - Log.d("MainActivity", "initUserConfig: accounts: $accounts") - if (accounts.isNotEmpty()) { - val currentAccount = accounts.find { it.userId == UserConfig.currentUserId } - if (currentAccount != null) { - UserConfig.parse(currentAccount) - } - - openMainScreen() - } else { - openMainScreen() - } - } - } - - private fun openMainScreen() { - if (savedInstanceState != null) return - - var needToOpenSettings = false - - if (intent.dataString != null) { - intent.dataString?.let { data -> - if (data == "shortcut_settings") { - needToOpenSettings = true - } - } - } - - if (intent.hasExtra("data")) { - if (intent.getStringExtra("data") == "open_settings") { - needToOpenSettings = true - } - } - - if (needToOpenSettings) { - router.newRootScreen(Screens.Settings()) - } else { - router.newRootScreen(Screens.Main()) - } - } - - override fun onDestroy() { - super.onDestroy() - stopServices() - updatesParser.clearListeners() - isOnlineServiceWasLaunched = false - savedInstanceState = null - } - - companion object { - val longPollState = MutableStateFlow(LongPollState.Stop) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/ServicesState.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/activity/ServicesState.kt deleted file mode 100644 index 8a16a8ae..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/ServicesState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.screens.main.activity - -sealed class ServicesState { - object Started : ServicesState() - object Stopped : ServicesState() - object Unknown : ServicesState() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/di/MainModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/di/MainModule.kt deleted file mode 100644 index 1305bdb7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/di/MainModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.main.di - -import com.meloda.fast.screens.main.MainViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val mainModule = module { - viewModelOf(::MainViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt deleted file mode 100644 index 52b55b52..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt +++ /dev/null @@ -1,628 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.content.Context -import android.content.res.Resources -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.Space -import androidx.appcompat.widget.AppCompatTextView -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.view.isNotEmpty -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import androidx.core.view.updateMarginsRelative -import androidx.core.view.updatePadding -import com.bumptech.glide.Priority -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VkUtils -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.attachments.VkAttachment -import com.meloda.fast.api.model.attachments.VkAudio -import com.meloda.fast.api.model.attachments.VkCall -import com.meloda.fast.api.model.attachments.VkFile -import com.meloda.fast.api.model.attachments.VkGift -import com.meloda.fast.api.model.attachments.VkGraffiti -import com.meloda.fast.api.model.attachments.VkLink -import com.meloda.fast.api.model.attachments.VkPhoto -import com.meloda.fast.api.model.attachments.VkSticker -import com.meloda.fast.api.model.attachments.VkStory -import com.meloda.fast.api.model.attachments.VkVideo -import com.meloda.fast.api.model.attachments.VkVoiceMessage -import com.meloda.fast.api.model.attachments.VkWall -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.databinding.ItemMessageAttachmentAudioBinding -import com.meloda.fast.databinding.ItemMessageAttachmentCallBinding -import com.meloda.fast.databinding.ItemMessageAttachmentFileBinding -import com.meloda.fast.databinding.ItemMessageAttachmentForwardsBinding -import com.meloda.fast.databinding.ItemMessageAttachmentGeoBinding -import com.meloda.fast.databinding.ItemMessageAttachmentGiftBinding -import com.meloda.fast.databinding.ItemMessageAttachmentGraffitiBinding -import com.meloda.fast.databinding.ItemMessageAttachmentLinkBinding -import com.meloda.fast.databinding.ItemMessageAttachmentPhotoBinding -import com.meloda.fast.databinding.ItemMessageAttachmentReplyBinding -import com.meloda.fast.databinding.ItemMessageAttachmentStickerBinding -import com.meloda.fast.databinding.ItemMessageAttachmentStoryBinding -import com.meloda.fast.databinding.ItemMessageAttachmentVideoBinding -import com.meloda.fast.databinding.ItemMessageAttachmentVoiceBinding -import com.meloda.fast.databinding.ItemMessageAttachmentWallPostBinding -import com.meloda.fast.ext.ImageLoader.clear -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.TypeTransformations -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.orDots -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.toggleVisibilityIfHasContent -import com.meloda.fast.ext.visible -import com.meloda.fast.model.base.parseString -import com.meloda.fast.util.AndroidUtils -import java.text.SimpleDateFormat -import java.util.Locale -import kotlin.math.roundToInt - -class AttachmentInflater constructor( - private val context: Context, - private val container: LinearLayoutCompat, - private val replyContainer: FrameLayout, - private val timeReadContainer: View, - private val message: VkMessage, - private val profiles: Map, - private val groups: Map, -) { - private lateinit var attachments: List - - private val inflater = LayoutInflater.from(context) - - private val colorPrimary = ContextCompat.getColor( - context, - R.color.colorPrimary - ) - private val colorBackground = ContextCompat.getColor( - context, - R.color.colorBackground - ) - private val colorSecondary = ContextCompat.getColor( - context, - R.color.colorSecondary - ) - - private val timeReadBackground = ContextCompat.getDrawable( - context, - R.drawable.time_read_indicator_on_attachments_background - ) - - private var photoClickListener: ((url: String) -> Unit)? = null - private var replyClickListener: ((replyMessage: VkMessage) -> Unit)? = null - private var forwardsClickListener: ((forwards: List) -> Unit)? = null - - private val displayMetrics get() = Resources.getSystem().displayMetrics - - fun withPhotoClickListener(block: ((url: String) -> Unit)?): AttachmentInflater { - this.photoClickListener = block - return this - } - - fun withReplyClickListener(block: ((replyMessage: VkMessage) -> Unit)?): AttachmentInflater { - this.replyClickListener = block - return this - } - - fun withForwardsClickListener(block: ((forwards: List) -> Unit)?): AttachmentInflater { - this.forwardsClickListener = block - return this - } - - fun inflate() { - container.removeAllViews() - replyContainer.removeAllViews() - - replyContainer.toggleVisibility(message.hasReply()) - container.toggleVisibility( - !message.attachments.isNullOrEmpty() - || message.hasForwards() - || message.hasGeo() - ) - - timeReadContainer.run { - updateLayoutParams { - val margin = (if (container.isVisible) 6 else 2).dpToPx() - updateMarginsRelative(end = margin, bottom = margin) - } - - background = if (container.isVisible) timeReadBackground else null - } - - if (message.hasReply()) { - reply(requireNotNull(message.replyMessage)) - } - - if (message.hasForwards()) { - forwards(requireNotNull(message.forwards)) - } - - if (message.hasGeo()) { - geo(requireNotNull(message.geo)) - } - - if (message.attachments.isNullOrEmpty()) return - attachments = requireNotNull(message.attachments) - - if (attachments.size == 1) { - when (val attachment = attachments[0]) { - is VkSticker -> return sticker(attachment) - is VkWall -> return wall(attachment) - is VkVoiceMessage -> return voice(attachment) - is VkCall -> return call(attachment) - is VkGraffiti -> return graffiti(attachment) - is VkGift -> return gift(attachment) - is VkStory -> return story(attachment) - } - } - - if (attachments.size > 1) { - if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkPhoto) { - return attachments.forEach { photo(it as VkPhoto) } - } - - if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkVideo) { - return attachments.forEach { video(it as VkVideo) } - } - - if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkAudio) { - return attachments.forEach { audio(it as VkAudio) } - } - - if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkFile) { - return attachments.forEach { file(it as VkFile) } - } - } - - attachments.forEach { attachment -> - when (attachment) { - is VkPhoto -> photo(attachment) - is VkVideo -> video(attachment) - is VkAudio -> audio(attachment) - is VkFile -> file(attachment) - is VkLink -> link(attachment) - - else -> unknown(attachment) - } - } - } - - private fun unknown(attachment: VkAttachment) { - val attachmentType = attachment.javaClass.name - Log.e( - "Attachment inflater", - "Unknown attachment type: $attachmentType" - ) - - val textView = AppCompatTextView(context) - textView.text = attachmentType - - container.addView(textView) - } - - private fun reply(replyMessage: VkMessage) { - val binding = ItemMessageAttachmentReplyBinding.inflate(inflater, replyContainer, true) - binding.root.setOnClickListener { replyClickListener?.invoke(replyMessage) } - - val attachmentText = (VkUtils.getAttachmentText( - message = replyMessage - ))?.parseString(context) - - val forwardsMessage = (if (replyMessage.text == null) VkUtils.getForwardsText( - message = replyMessage - ) else null)?.parseString(context) - - val messageText = attachmentText ?: forwardsMessage ?: (replyMessage.text.orDots()).run { - VkUtils.prepareMessageText(this) - } - - binding.text.text = VkUtils.visualizeMentions( - messageText = messageText, - mentionColor = colorPrimary - ) - - val replyUserGroup = VkUtils.getMessageUserGroup(replyMessage, profiles, groups) - - val fromUser: VkUser? = replyUserGroup.first - val fromGroup: VkGroup? = replyUserGroup.second - - val title = VkUtils.getMessageTitle(replyMessage, fromUser, fromGroup) - binding.title.text = title.orDots() - } - - private fun forwards(forwards: List) { - val binding = ItemMessageAttachmentForwardsBinding.inflate(inflater, container, true) - - binding.root.setOnClickListener { forwardsClickListener?.invoke(forwards) } - } - - private fun geo(geo: BaseVkMessage.Geo) { - val binding = ItemMessageAttachmentGeoBinding.inflate(inflater, container, true) - - binding.location.text = geo.place.title - binding.location.toggleVisibilityIfHasContent() - } - - private fun photo(photo: VkPhoto) { - val size = photo.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807) ?: return - - val specRatio = size.width.toFloat() / size.height.toFloat() - val widthMultiplier: Float = when { - specRatio > 1 -> 0.7F - specRatio < 1 -> 0.45F - else -> 0.35F - } - val ratio = "${size.width}:${size.height}" - - val spacer = Space(context).apply { - layoutParams = - LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx()) - } - - if (container.isNotEmpty()) { - container.addView(spacer) - } - - val binding = ItemMessageAttachmentPhotoBinding.inflate(inflater, container, true) - - val cornersRadius = 17.dpToPx().toFloat() - - binding.border.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius) - - updateLayoutParams { - width = (displayMetrics.widthPixels * widthMultiplier).roundToInt() - dimensionRatio = ratio - } - loadWithGlide { - imageDrawable = ColorDrawable(colorSecondary) - loadPriority = Priority.IMMEDIATE - cacheStrategy = DiskCacheStrategy.NONE - } - } - - binding.image.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F) - - setOnClickListener { - photo.getMaxSize()?.let { size -> photoClickListener?.invoke(size.url) } - } - - loadWithGlide { - imageUrl = size.url - crossFade = true - placeholderDrawable = ColorDrawable(colorBackground) - loadPriority = Priority.LOW - } - } - } - - private fun video(video: VkVideo) { - val spacer = Space(context).apply { - layoutParams = - LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx()) - } - if (container.isNotEmpty()) { - container.addView(spacer) - } - - val size = video.imageForWidthAtLeast(300) ?: return - val binding = ItemMessageAttachmentVideoBinding.inflate(inflater, container, true) - - val specRatio = size.width.toFloat() / size.height.toFloat() - val widthMultiplier: Float = when { - specRatio > 1 -> 0.7F - specRatio < 1 -> 0.45F - else -> 0.35F - } - val ratio = "${size.width}:${size.height}" - - val cornersRadius = 17.dpToPx().toFloat() - - binding.border.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius) - - updateLayoutParams { - width = (displayMetrics.widthPixels * widthMultiplier).roundToInt() - dimensionRatio = ratio - } - loadWithGlide { - imageDrawable = ColorDrawable(colorSecondary) - loadPriority = Priority.IMMEDIATE - cacheStrategy = DiskCacheStrategy.NONE - } - } - - binding.image.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F) - - loadWithGlide { - imageUrl = size.url - crossFade = true - placeholderDrawable = ColorDrawable(colorBackground) - loadPriority = Priority.LOW - } - } - } - - private fun audio(audio: VkAudio) { - val binding = ItemMessageAttachmentAudioBinding.inflate(inflater, container, true) - - binding.title.text = audio.title - binding.artist.text = "%s | %s".format( - audio.artist, - SimpleDateFormat("mm:ss", Locale.getDefault()).format(audio.duration * 1000L) - ) - } - - private fun file(file: VkFile) { - val binding = ItemMessageAttachmentFileBinding.inflate(inflater, container, true) - - binding.title.text = file.title - binding.size.text = "%s | %s".format( - AndroidUtils.bytesToHumanReadableSize(file.size.toDouble()), - file.ext.uppercase() - ) - } - - private fun link(link: VkLink) { - val binding = ItemMessageAttachmentLinkBinding.inflate( - inflater, container, true - ) - - binding.title.text = link.title - binding.title.toggleVisibility(!link.title.isNullOrBlank()) - - binding.caption.text = link.caption - binding.caption.toggleVisibility(!link.caption.isNullOrBlank()) - - link.photo?.getSizeOrSmaller('y')?.let { size -> - binding.preview.loadWithGlide { - imageUrl = size.url - crossFade = true - } - binding.linkIcon.gone() - return - } - - binding.preview.setImageDrawable( - ColorDrawable( - ContextCompat.getColor( - context, - R.color.a3_200 - ) - ) - ) - binding.linkIcon.visible() - } - - private fun sticker(sticker: VkSticker) { - val binding = ItemMessageAttachmentStickerBinding.inflate(inflater, container, true) - - val url = sticker.urlForSize(352) - - binding.image.run { - val size = 140.dpToPx() - - layoutParams = LinearLayoutCompat.LayoutParams(size, size) - - loadWithGlide { - imageUrl = url - crossFade = true - } - } - } - - private fun wall(wall: VkWall) { - val binding = ItemMessageAttachmentWallPostBinding.inflate(inflater, container, true) - - val group = if (wall.fromId > 0) null else groups[wall.fromId] - val user = if (wall.fromId < 0) null else profiles[wall.fromId] - - val postTitleRes = when { - group != null && user == null -> R.string.post_type_community - user != null && group == null -> R.string.post_type_user - else -> R.string.post_type_unknown - } - - val avatar = when { - group == null && user != null -> user.photo200 - user == null && group != null -> group.photo200 - else -> null - } - - val title = (when { - group == null && user != null -> user.fullName - user == null && group != null -> group.name - else -> null - }).orDots() - - binding.postTitle.text = context.getString(postTitleRes) - binding.postTitle.gone() - - binding.avatar.toggleVisibility(group != null || user != null) - - if (binding.avatar.isVisible) { - binding.avatar.loadWithGlide { - imageUrl = avatar - crossFade = true - } - } else { - binding.avatar.clear() - } - - binding.title.text = title - - binding.date.text = SimpleDateFormat( - "dd.MM.yyyy HH:mm", - Locale.getDefault() - ).format(wall.date * 1000L) - } - - private fun voice(voiceMessage: VkVoiceMessage) { - val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, container, true) - - if (message.isOut) { - val padding = 6.dpToPx() - binding.root.updatePadding( - bottom = padding, - left = padding - ) - } - val waveform = IntArray(voiceMessage.waveform.size) - voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i } - - binding.waveform.sample = waveform - binding.waveform.maxProgress = 100f - binding.waveform.progress = 100f - - binding.duration.text = SimpleDateFormat( - "mm:ss", - Locale.getDefault() - ).format(voiceMessage.duration * 1000L) - } - - private fun call(call: VkCall) { - val binding = ItemMessageAttachmentCallBinding.inflate(inflater, container, true) - - if (message.isOut) - binding.root.updatePadding( - bottom = 5.dpToPx(), - left = 6.dpToPx() - ) - - val callType = - context.getString( - if (call.initiatorId == UserConfig.userId) R.string.message_call_type_outgoing - else R.string.message_call_type_incoming - ) - - binding.type.text = callType - - var callState = - context.getString( - if (call.state == "reached") R.string.message_call_state_ended - else if (call.state == "canceled_by_initiator") { - if (call.initiatorId == UserConfig.userId) R.string.message_call_state_cancelled - else R.string.message_call_state_missed - } else R.string.message_call_unknown - ) - - if (callState == context.getString(R.string.message_call_unknown)) callState = call.state - - binding.state.text = callState - } - - private fun graffiti(graffiti: VkGraffiti) { - val binding = ItemMessageAttachmentGraffitiBinding.inflate(inflater, container, true) - - val url = graffiti.url - - val size = 140.dpToPx() - - val heightCoefficient = graffiti.height / size.toFloat() - - binding.image.run { - layoutParams = LinearLayoutCompat.LayoutParams( - size, - (graffiti.height / heightCoefficient).roundToInt() - ) - - loadWithGlide { - imageUrl = url - crossFade = true - } - } - } - - private fun gift(gift: VkGift) { - val binding = ItemMessageAttachmentGiftBinding.inflate(inflater, container, true) - - val url = gift.thumb256 ?: gift.thumb96 ?: gift.thumb48 - - binding.image.run { - val size = 140.dpToPx() - - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(12.dpToPx().toFloat()) - - layoutParams = LinearLayoutCompat.LayoutParams(size, size) - - loadWithGlide { - imageUrl = url - crossFade = true - } - } - } - - private fun story(story: VkStory) { - val binding = ItemMessageAttachmentStoryBinding.inflate(inflater, container, true) - - val photoUrl = story.photo?.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807)?.url - - val dimmerDrawable = - ContextCompat.getDrawable(context, R.drawable.ic_message_attachment_story_image_dimmer) - - val cornersRadius = 24.dpToPx() - - binding.caption.updateLayoutParams { - val margin = cornersRadius / 2 - updateMarginsRelative( - top = margin, - start = margin, - end = margin, - bottom = margin - ) - } - - binding.dimmer.loadWithGlide { - imageDrawable = dimmerDrawable - transformations = listOf(TypeTransformations.RoundedCornerCrop(cornersRadius)) - loadPriority = Priority.IMMEDIATE - cacheStrategy = DiskCacheStrategy.NONE - } - - binding.image.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius.toFloat()) - - loadWithGlide { - imageUrl = photoUrl - crossFade = true - placeholderDrawable = ColorDrawable(Color.GRAY) - } - } - - if (story.ownerId == UserConfig.userId) { - binding.caption.text = context.getString(R.string.message_attachment_story_your_story) - } else { - val storyOwnerUser = if (story.isFromUser()) profiles[story.ownerId] else null - val storyOwnerGroup = if (story.isFromGroup()) groups[story.ownerId] else null - - val ownerName = when { - storyOwnerUser != null -> storyOwnerUser.fullName - storyOwnerGroup != null -> storyOwnerGroup.name - else -> null - } - - binding.caption.text = context.getString( - R.string.message_attachment_story_story_from, - ownerName - ) - binding.caption.toggleVisibility(ownerName != null) - binding.dimmer.toggleVisibility(binding.caption.isVisible) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentsAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentsAdapter.kt deleted file mode 100644 index 97c15eb8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentsAdapter.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DiffUtil -import com.google.android.material.shape.ShapeAppearanceModel -import com.meloda.fast.R -import com.meloda.fast.api.model.attachments.VkAttachment -import com.meloda.fast.api.model.attachments.VkAudio -import com.meloda.fast.api.model.attachments.VkFile -import com.meloda.fast.api.model.attachments.VkPhoto -import com.meloda.fast.api.model.attachments.VkVideo -import com.meloda.fast.base.adapter.BaseAdapter -import com.meloda.fast.base.adapter.BaseHolder -import com.meloda.fast.databinding.ItemUploadedAttachmentAudioBinding -import com.meloda.fast.databinding.ItemUploadedAttachmentFileBinding -import com.meloda.fast.databinding.ItemUploadedAttachmentPhotoBinding -import com.meloda.fast.databinding.ItemUploadedAttachmentVideoBinding -import com.meloda.fast.ext.ImageLoader.clear -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.visible - -class AttachmentsAdapter( - context: Context, - preAddedValues: List, - private var onRemoveClickedListener: ((position: Int) -> Unit)? = null, -) : BaseAdapter( - context, comparator, preAddedValues -) { - - private companion object { - - private const val TypePhoto = 1 - private const val TypeVideo = 2 - private const val TypeAudio = 3 - private const val TypeFile = 4 - - private val comparator = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: VkAttachment, newItem: VkAttachment): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: VkAttachment, newItem: VkAttachment): Boolean { - return false - } - } - } - - private val colorPrimaryVariant = ContextCompat.getColor(context, R.color.colorPrimaryVariant) - - open inner class Holder(v: View) : BaseHolder(v) - - override fun getItemViewType(position: Int): Int { - return when (getItem(position)) { - is VkPhoto -> TypePhoto - is VkVideo -> TypeVideo - is VkAudio -> TypeAudio - is VkFile -> TypeFile - else -> super.getItemViewType(position) - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return when (viewType) { - TypePhoto -> PhotoViewHolder( - ItemUploadedAttachmentPhotoBinding.inflate(inflater, parent, false) - ) - TypeVideo -> VideoViewHolder( - ItemUploadedAttachmentVideoBinding.inflate(inflater, parent, false) - ) - TypeAudio -> AudioViewHolder( - ItemUploadedAttachmentAudioBinding.inflate(inflater, parent, false) - ) - TypeFile -> FileViewHolder( - ItemUploadedAttachmentFileBinding.inflate(inflater, parent, false) - ) - else -> Holder(View(context)) - } - } - - inner class PhotoViewHolder( - private val binding: ItemUploadedAttachmentPhotoBinding, - ) : Holder(binding.root) { - - init { - binding.image.shapeAppearanceModel = - binding.image.shapeAppearanceModel.withCornerSize(18.dpToPx().toFloat()) - } - - override fun bind(position: Int) { - val photo = getItem(position) as VkPhoto - - binding.progressBar.visible() - - val onDoneAction = { binding.progressBar.gone() } - - binding.image.loadWithGlide { - imageUrl = photo.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807)?.url - crossFade = true - placeholderColor = colorPrimaryVariant - onLoadedAction = onDoneAction - onFailedAction = onDoneAction - } - - binding.close.setOnClickListener { - onRemoveClickedListener?.invoke(position) - } - } - } - - inner class VideoViewHolder( - private val binding: ItemUploadedAttachmentVideoBinding, - ) : Holder(binding.root) { - init { - val cornerSizedShapeAppearanceModel = ShapeAppearanceModel().withCornerSize( - 18.dpToPx().toFloat() - ) - - binding.image.shapeAppearanceModel = cornerSizedShapeAppearanceModel - binding.coloredBackground.shapeAppearanceModel = cornerSizedShapeAppearanceModel - } - - override fun bind(position: Int) { - val video = getItem(position) as VkVideo - - binding.title.text = video.title - - val previewSrc = video.imageForWidthAtLeast(300) - binding.image.toggleVisibility(previewSrc != null) - binding.coloredBackground.toggleVisibility(previewSrc == null) - binding.videoIcon.toggleVisibility(previewSrc == null) - - if (previewSrc != null) { - binding.progressBar.visible() - - binding.image.loadWithGlide { - imageUrl = previewSrc.url - crossFade = true - placeholderColor = colorPrimaryVariant - onLoadedAction = { binding.progressBar.gone() } - onFailedAction = { showPlaceholder() } - } - } else { - binding.progressBar.gone() - binding.image.clear() - } - - binding.close.setOnClickListener { - onRemoveClickedListener?.invoke(position) - } - } - - private fun showPlaceholder() { - binding.coloredBackground.visible() - binding.videoIcon.visible() - binding.image.clear() - binding.image.gone() - binding.progressBar.gone() - } - } - - inner class AudioViewHolder( - private val binding: ItemUploadedAttachmentAudioBinding, - ) : Holder(binding.root) { - init { - binding.coloredBackground.shapeAppearanceModel = - binding.coloredBackground.shapeAppearanceModel.withCornerSize(18.dpToPx().toFloat()) - } - - override fun bind(position: Int) { - val audio = getItem(position) as VkAudio - - binding.title.text = audio.title - - binding.close.setOnClickListener { - onRemoveClickedListener?.invoke(position) - } - } - } - - inner class FileViewHolder( - private val binding: ItemUploadedAttachmentFileBinding, - ) : Holder(binding.root) { - - init { - val cornerSizedShapeAppearanceModel = ShapeAppearanceModel().withCornerSize( - 18.dpToPx().toFloat() - ) - - binding.image.shapeAppearanceModel = cornerSizedShapeAppearanceModel - binding.coloredBackground.shapeAppearanceModel = cornerSizedShapeAppearanceModel - } - - override fun bind(position: Int) { - val file = getItem(position) as VkFile - - binding.title.text = file.title - - val previewSrc = file.preview?.photo?.sizes?.get(0) - binding.image.toggleVisibility(previewSrc != null) - binding.coloredBackground.toggleVisibility(previewSrc == null) - binding.fileIcon.toggleVisibility(previewSrc == null) - - if (previewSrc != null) { - binding.progressBar.visible() - - binding.image.loadWithGlide { - imageUrl = previewSrc.src - crossFade = true - placeholderColor = colorPrimaryVariant - onLoadedAction = { binding.progressBar.gone() } - onFailedAction = { showPlaceholder() } - } - } else { - binding.progressBar.gone() - binding.image.clear() - } - - binding.close.setOnClickListener { - onRemoveClickedListener?.invoke(position) - } - } - - private fun showPlaceholder() { - binding.coloredBackground.visible() - binding.fileIcon.visible() - binding.image.clear() - binding.image.gone() - binding.progressBar.gone() - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/ForwardedMessagesFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/ForwardedMessagesFragment.kt deleted file mode 100644 index 8c68224a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/ForwardedMessagesFragment.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.os.Bundle -import android.view.View -import androidx.core.os.bundleOf -import by.kirich1409.viewbindingdelegate.viewBinding -import com.github.terrakok.cicerone.Router -import com.meloda.fast.R -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.domain.VkConversationDomain -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.common.Screens -import com.meloda.fast.databinding.FragmentForwardedMessagesBinding -import com.meloda.fast.ext.getParcelableArrayListCompat -import com.meloda.fast.ext.getParcelableCompat -import com.meloda.fast.ext.getSerializableCompat -import dev.chrisbanes.insetter.applyInsetter -import org.koin.android.ext.android.inject - -class ForwardedMessagesFragment : BaseFragment(R.layout.fragment_forwarded_messages) { - - private val router: Router by inject() - - private val binding by viewBinding(FragmentForwardedMessagesBinding::bind) - - private var conversation: VkConversationDomain? = null - private var messages: List = emptyList() - private var profiles = hashMapOf() - private var groups = hashMapOf() - - private val adapter: MessagesHistoryAdapter by lazy { - MessagesHistoryAdapter( - this, requireNotNull(conversation), profiles, groups - ) - } - - @Suppress("UNCHECKED_CAST") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - requireArguments().run { - conversation = getParcelableCompat(ArgConversation, VkConversationDomain::class.java) - - messages = getParcelableArrayListCompat(ArgMessages, VkMessage::class.java) - ?: emptyList() - - profiles = - getSerializableCompat(ArgProfiles, HashMap::class.java) as? HashMap - ?: hashMapOf() - groups = - getSerializableCompat(ArgGroups, HashMap::class.java) as? HashMap - ?: hashMapOf() - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.recyclerView.applyInsetter { - type(navigationBars = true) { padding() } - } - - binding.toolbar.applyInsetter { - type(statusBars = true) { padding() } - } - binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } - - fillRecyclerView() - } - - private fun fillRecyclerView() { - adapter.setItems(messages) - binding.recyclerView.adapter = adapter - } - - fun scrollToMessage(messageId: Int) { - adapter.searchMessageIndex(messageId)?.let { index -> - binding.recyclerView.scrollToPosition(index) - } - } - - fun openForwardsScreen( - conversation: VkConversationDomain, - messages: List, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf() - ) { - router.navigateTo( - Screens.ForwardedMessages(conversation, messages, profiles, groups) - ) - } - - companion object { - private const val ArgConversation = "conversation" - private const val ArgMessages = "messages" - private const val ArgProfiles = "profiles" - private const val ArgGroups = "groups" - - fun newInstance( - conversation: VkConversationDomain, - messages: List, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf() - ): ForwardedMessagesFragment { - val fragment = ForwardedMessagesFragment() - fragment.arguments = bundleOf( - ArgConversation to conversation, - ArgMessages to messages, - ArgProfiles to profiles, - ArgGroups to groups - ) - - return fragment - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt deleted file mode 100644 index bbbb680d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt +++ /dev/null @@ -1,377 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.util.Log -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.core.util.ObjectsCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.DiffUtil -import com.meloda.fast.R -import com.meloda.fast.api.VkUtils -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.attachments.VkPhoto -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.base.adapter.BaseAdapter -import com.meloda.fast.base.adapter.BaseHolder -import com.meloda.fast.databinding.ItemMessageInBinding -import com.meloda.fast.databinding.ItemMessageOutBinding -import com.meloda.fast.databinding.ItemMessageServiceBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.dpToPx - -class MessagesHistoryAdapter constructor( - context: Context, - val conversation: VkConversationDomain, - val profiles: HashMap = hashMapOf(), - val groups: HashMap = hashMapOf(), -) : BaseAdapter( - context, - Comparator -) { - - constructor( - fragment: MessagesHistoryFragment, - conversation: VkConversationDomain, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf(), - ) : this(fragment.requireContext(), conversation, profiles, groups) { - this.messagesHistoryFragment = fragment - } - - constructor( - fragment: ForwardedMessagesFragment, - conversation: VkConversationDomain, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf(), - ) : this(fragment.requireContext(), conversation, profiles, groups) { - this.isForwards = true - this.forwardedMessagesFragment = fragment - } - - private var isForwards: Boolean = false - - private var messagesHistoryFragment: MessagesHistoryFragment? = null - private var forwardedMessagesFragment: ForwardedMessagesFragment? = null - - var avatarLongClickListener: ((position: Int) -> Unit)? = null - - override fun getItemViewType(position: Int): Int { - return when (val item = getItem(position)) { - is VkMessage -> { - return when { - item.action != null -> TypeService - item.isOut -> TypeOutgoing - !item.isOut -> TypeIncoming - else -> -1 - } - } - else -> -1 - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder { - return when (viewType) { - TypeService -> ServiceMessage( - ItemMessageServiceBinding.inflate(inflater, parent, false) - ) - TypeOutgoing -> OutgoingMessage( - ItemMessageOutBinding.inflate(inflater, parent, false) - ) - TypeIncoming -> IncomingMessage( - ItemMessageInBinding.inflate(inflater, parent, false) - ) - else -> throw IllegalStateException("Wrong viewType: $viewType") - } - } - - override fun onBindViewHolder(holder: BasicHolder, position: Int) { - if (holder is Header || holder is Footer) { - Log.d( - "MessagesHistoryAdapter", - "onBindViewHolder: index $position, holder: ${holder.javaClass.simpleName}. Skip" - ) - return - } - - Log.d( - "MessagesHistoryAdapter", - "onBindViewHolder: index $position, holder: ${holder.javaClass.simpleName}. Bind" - ) - - initListeners(holder.itemView, position) - holder.bind(position) - } - - open inner class BasicHolder(v: View = View(context)) : BaseHolder(v) - - inner class Header(v: View) : BasicHolder(v) - - inner class Footer(v: View) : BasicHolder(v) - - inner class IncomingMessage( - private val binding: ItemMessageInBinding, - ) : BasicHolder(binding.root) { - - override fun bind(position: Int, payloads: MutableList?) { - val message = getItem(position) as VkMessage - - val prevMessage = getOrNull(position - 1) - val nextMessage = getOrNull(position + 1) - - MessagesPreparator( - context = context, - position = position, - adapterClickListener = itemClickListener, - payloads = payloads, - - root = binding.root, - - conversation = conversation, - message = message, - prevMessage = prevMessage, - nextMessage = nextMessage, - - title = binding.title, - - avatar = binding.avatar, - bubble = binding.bubble, - text = binding.text, - spacer = binding.spacer, - messageState = binding.messageState, - time = binding.time, - - replyContainer = binding.replyContainer, - attachmentContainer = binding.attachmentContainer, - timeReadContainer = binding.timeReadContainer, - - profiles = profiles, - groups = groups, - - isForwards = isForwards - ) - .withPhotoClickListener { - Intent(Intent.ACTION_VIEW, Uri.parse(it)).run { - context.startActivity(this) - } - } - .withReplyClickListener { - messagesHistoryFragment?.scrollToMessage(it.id) - forwardedMessagesFragment?.scrollToMessage(it.id) - } - .withForwardsClickListener { messages -> - messagesHistoryFragment?.openForwardsScreen( - conversation, messages, profiles, groups - ) - forwardedMessagesFragment?.openForwardsScreen( - conversation, messages, profiles, groups - ) - } - .prepare() - - binding.avatar.setOnLongClickListener { - avatarLongClickListener?.invoke(position) - true - } - } - } - - inner class OutgoingMessage( - private val binding: ItemMessageOutBinding, - ) : BasicHolder(binding.root) { - - override fun bind(position: Int, payloads: MutableList?) { - val message = getItem(position) - val prevMessage = getOrNull(position - 1) - - MessagesPreparator( - context = context, - position = position, - adapterClickListener = itemClickListener, - payloads = payloads, - root = binding.root, - conversation = conversation, - message = message, - prevMessage = prevMessage, - - bubble = binding.bubble, - text = binding.text, - spacer = binding.spacer, - messageState = binding.messageState, - time = binding.time, - - timeReadContainer = binding.timeReadContainer, - replyContainer = binding.replyContainer, - attachmentContainer = binding.attachmentContainer, - - profiles = profiles, - groups = groups, - - isForwards = isForwards - ) - .withPhotoClickListener { - Intent(Intent.ACTION_VIEW, Uri.parse(it)).run { - context.startActivity(this) - } - } - .withReplyClickListener { - messagesHistoryFragment?.scrollToMessage(it.id) - forwardedMessagesFragment?.scrollToMessage(it.id) - } - .withForwardsClickListener { messages -> - messagesHistoryFragment?.openForwardsScreen( - conversation, messages, profiles, groups - ) - forwardedMessagesFragment?.openForwardsScreen( - conversation, messages, profiles, groups - ) - } - .prepare() - } - } - - inner class ServiceMessage( - private val binding: ItemMessageServiceBinding, - ) : BasicHolder(binding.root) { - - private val youPrefix = context.getString(R.string.you_message_prefix) - - init { - binding.photo.shapeAppearanceModel = - binding.photo.shapeAppearanceModel.withCornerSize(4.dpToPx().toFloat()) - } - - override fun bind(position: Int) { - val message = getItem(position) as VkMessage - - val messageUser = - if (message.isUser()) profiles[message.fromId] - else null - - val messageGroup = - if (message.isGroup()) groups[message.fromId] - else null - - message.action ?: return - - binding.message.text = VkUtils.getActionMessageText( - context = context, - message = message, - youPrefix = youPrefix, - messageUser = messageUser, - messageGroup = messageGroup, - action = message.getPreparedAction(), - actionUser = null, - actionGroup = null, - ) - - val attachments = message.attachments ?: return - attachments[0].let { attachment -> - if (attachment !is VkPhoto) return@let - - binding.photo.isVisible = true - - val size = attachment.getSizeOrSmaller('y') ?: return@let - - binding.photo.layoutParams = LinearLayoutCompat.LayoutParams( - size.width, - size.height - ) - - binding.photo.loadWithGlide { - imageUrl = size.url - crossFade = true - placeholderDrawable = ColorDrawable(Color.LTGRAY) - } - - binding.photo.setOnClickListener { - Intent(Intent.ACTION_VIEW, Uri.parse(size.url)).run { - context.startActivity(this) - } - } - } - } - } - - fun containsUnreadMessages(isOutgoingMessages: Boolean = false): Boolean { - for (i in indices) { - val item = getItem(i) - if (item !is VkMessage) continue - - if (item.isOut == isOutgoingMessages && !item.isRead(conversation)) { - return true - } - } - return false - } - - fun containsRandomId(randomId: Int): Boolean { - if (randomId == 0) return false - for (i in indices) { - val item = getItem(i) - if (item !is VkMessage) continue - - if (item.randomId == randomId) return true - } - - return false - } - - fun containsId(id: Int): Boolean { - for (i in indices) { - val item = getItem(i) - if (item !is VkMessage) continue - - if (item.id == id) return true - } - - return false - } - - fun searchMessageIndex(messageId: Int): Int? { - for (i in indices) { - val message = getItem(i) - if (message is VkMessage && message.id == messageId) return i - } - - return null - } - - fun searchMessageById(messageId: Int): VkMessage? { - for (i in indices) { - val message = getItem(i) - if (message is VkMessage && message.id == messageId) return message - } - - return null - } - - companion object { - private const val TypeService = 1 - private const val TypeIncoming = 3 - private const val TypeOutgoing = 4 - - private val Comparator = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: VkMessage, - newItem: VkMessage, - ): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame( - oldItem: VkMessage, - newItem: VkMessage, - ): Boolean { - return ObjectsCompat.equals(oldItem, newItem) && (oldItem.state == newItem.state) - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt deleted file mode 100644 index f02915f6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt +++ /dev/null @@ -1,1531 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.animation.ValueAnimator -import android.content.Context -import android.net.Uri -import android.os.Bundle -import android.os.Environment -import android.provider.OpenableColumns -import android.util.Log -import android.view.HapticFeedbackConstants -import android.view.View -import android.view.animation.LinearInterpolator -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.widget.PopupMenu -import androidx.core.animation.doOnEnd -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.core.view.updatePaddingRelative -import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import by.kirich1409.viewbindingdelegate.viewBinding -import com.bumptech.glide.Glide -import com.github.terrakok.cicerone.Router -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.common.net.MediaType -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VkUtils -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.attachments.VkAttachment -import com.meloda.fast.api.model.attachments.VkPhoto -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.base.viewmodel.BaseViewModelFragment -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.Screens -import com.meloda.fast.data.files.FilesRepository -import com.meloda.fast.databinding.DialogMessageDeleteBinding -import com.meloda.fast.databinding.FragmentMessagesHistoryBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.clear -import com.meloda.fast.ext.doOnApplyWindowInsets -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.getParcelableCompat -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.hideKeyboard -import com.meloda.fast.ext.listenValue -import com.meloda.fast.ext.mimeType -import com.meloda.fast.ext.orDots -import com.meloda.fast.ext.sdk30AndUp -import com.meloda.fast.ext.selectLast -import com.meloda.fast.ext.showKeyboard -import com.meloda.fast.ext.trimmedText -import com.meloda.fast.ext.visible -import com.meloda.fast.model.base.parseString -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.AndroidUtils -import com.meloda.fast.util.ColorUtils -import com.meloda.fast.util.ShareContent -import com.meloda.fast.util.TimeUtils -import com.meloda.fast.view.SpaceItemDecoration -import dev.chrisbanes.insetter.applyInsetter -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.viewModel -import java.io.File -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.Timer -import kotlin.concurrent.schedule -import kotlin.math.abs -import kotlin.properties.Delegates -import kotlin.random.Random - -class MessagesHistoryFragment : - BaseViewModelFragment(R.layout.fragment_messages_history) { - - private val router: Router by inject() - - private val binding by viewBinding(FragmentMessagesHistoryBinding::bind) - override val viewModel: MessagesHistoryViewModel by viewModel() - - private var pickFile: Boolean = false - - private val attachmentsToLoad = mutableListOf() - - private val getContent = - registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uriList: List? -> - if (uriList.isNullOrEmpty()) { - return@registerForActivityResult - } - - if (uriList.size > 10) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage("Select no more than 10 files") - .setPositiveButton(R.string.ok, null) - .show() - return@registerForActivityResult - } - - viewLifecycleOwner.lifecycleScope.launch { - val uploadFlow = flow { - uriList.forEach { uri -> - processFileFromStorage(uri) - emit(null) - } - } - - uploadFlow.collect() - } - } - - - private val actionState = MutableStateFlow(Action.RECORD) - - private enum class Action { - RECORD, SEND, EDIT, DELETE - } - - private val user: VkUser? by lazy { - requireArguments().getParcelableCompat(ARG_USER, VkUser::class.java) - } - - private val group: VkGroup? by lazy { - requireArguments().getParcelableCompat(ARG_GROUP, VkGroup::class.java) - } - - private var conversation: VkConversationDomain by Delegates.notNull() - - private val adapter: MessagesHistoryAdapter by lazy { - MessagesHistoryAdapter(this, conversation).also { - it.itemClickListener = this::onItemClick - it.avatarLongClickListener = this::onAvatarLongClickListener - } - } - - private val attachmentsAdapter: AttachmentsAdapter by lazy { - AttachmentsAdapter( - requireContext(), - emptyList(), - onRemoveClickedListener = { position -> - removeAttachment(attachmentsAdapter[position]) - } - ) - } - - private var timestampTimer: Timer? = null - - private lateinit var attachmentController: AttachmentPanelController - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - conversation = requireNotNull( - requireArguments().getParcelableCompat( - ARG_CONVERSATION, - VkConversationDomain::class.java - ) - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val colorBackground = ContextCompat.getColor(requireContext(), R.color.colorBackground) - val alphaColorBackground = ColorUtils.alphaColor(colorBackground, 0.85F) - binding.bottomMessagePanel.setBackgroundColor(alphaColorBackground) - - binding.toolbar.startButtonClickAction = { - requireActivity().onBackPressedDispatcher.onBackPressed() - } - - attachmentController = AttachmentPanelController.init( - context = requireContext(), - adapter = adapter, - lifecycleOwner = viewLifecycleOwner, - binding = binding, - isAttachmentsEmpty = { attachmentsToLoad.isEmpty() } - ) - - val title = when { - conversation.isChat() -> conversation.conversationTitle - conversation.isUser() -> user?.toString() - conversation.isGroup() -> group?.name - else -> null - } - -// listOf( -// binding.bottomAlpha, -// binding.bottomGradient -// ).forEach { v -> -// v.applyInsetter { -// type(navigationBars = true) { padding() } -// } -// } - binding.bottomMessagePanel.applyInsetter { - type(navigationBars = true, ime = true) { padding(animated = true) } - } -// binding.recyclerView.applyInsetter { -// type(navigationBars = true, ime = true) { padding(animated = true) } -// } - binding.toolbar.applyInsetter { - type(statusBars = true) { padding() } - } - binding.toolbar.title = title.orDots() - binding.toolbar.setOnClickListener { - openChatInfoScreen(conversation, user, group) - } - - val status = when { - conversation.isChat() -> "${conversation.membersCount} members" - conversation.isUser() -> when { - // TODO: 9/15/2021 user normal time - user?.online == true -> "Online" - user?.lastSeen != null -> "Last seen at ${ - SimpleDateFormat( - "HH:mm", - Locale.getDefault() - ).format(user?.lastSeen!! * 1000L) - }" - - else -> if (user?.lastSeenStatus != null) "Last seen ${user?.lastSeenStatus!!}" else "Last seen recently" - } - - conversation.isGroup() -> if (group?.membersCount != null) "${group?.membersCount} members" else "Group" - else -> null - } - - binding.toolbar.subtitle = status.orDots() - - prepareAvatar() - - prepareViews() - - binding.recyclerView.adapter = adapter - - viewModel.loadHistory(conversation.id) - - binding.action.setOnClickListener { - performAction() - } - -// binding.recyclerView.addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom -> -// if (bottom >= oldBottom) return@addOnLayoutChangeListener -// checkIfNeedToScrollToBottom() -// } - - binding.unreadCounter.setOnClickListener { - binding.recyclerView.scrollToPosition(adapter.lastPosition) - } - - binding.recyclerView.setItemViewCacheSize(30) - - binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - val layoutManager = recyclerView.layoutManager as LinearLayoutManager - val firstPosition = layoutManager.findFirstVisibleItemPosition() - val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition() - - if (AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL, - true - ) && dy < 0 - ) { - binding.recyclerView.hideKeyboard() - } - - setUnreadCounterVisibility(lastPosition, dy) - - adapter.getOrNull(firstPosition)?.let { - binding.timestamp.visible() - - val showExactTime = AppGlobal.preferences.getBoolean(SettingsFragment.KEY_SHOW_EXACT_TIME_ON_TIME_STAMP, false) - - val exactTime = SimpleDateFormat( - "HH:mm", - Locale.getDefault() - ).format(it.date * 1000L) - - val time = "${ - TimeUtils.getLocalizedDate( - requireContext(), - it.date * 1000L - ) - }${if (showExactTime) ", $exactTime" else ""}" - - binding.timestamp.text = time - - if (timestampTimer != null) { - timestampTimer?.cancel() - timestampTimer = null - } - - timestampTimer = Timer() - timestampTimer?.schedule(2500) { - recyclerView.post { - if (getView() == null) return@post - binding.timestamp.gone() - } - } - } - - super.onScrolled(recyclerView, dx, dy) - } - }) - - binding.message.doAfterTextChanged { text -> - val canSend = text.toString().isNotBlank() || attachmentsToLoad.isNotEmpty() - - val newValue: Action = - when { - attachmentController.isEditing -> - if (text.isNullOrBlank() && attachmentsToLoad.isEmpty()) { - Action.DELETE - } else { - Action.EDIT - } - - canSend -> Action.SEND - else -> { - Action.RECORD - } - } - - actionState.update { newValue } - } - - actionState - .asStateFlow() - .flowWithLifecycle(lifecycle) - .onEach { state -> - binding.action.animate() - .scaleX(1.25f) - .scaleY(1.25f) - .setDuration(100) - .withEndAction { - if (getView() == null) return@withEndAction - - binding.action.animate() - .scaleX(1f) - .scaleY(1f) - .setDuration(100) - .start() - }.start() - - when (state) { - Action.RECORD -> { - binding.action.setImageResource(R.drawable.ic_round_mic_24) - } - - Action.SEND -> { - binding.action.setImageResource(R.drawable.ic_round_send_24) - } - - Action.EDIT -> { - binding.action.setImageResource(R.drawable.ic_round_done_24) - } - - Action.DELETE -> { - binding.action.setImageResource(R.drawable.ic_trash_can_outline_24) - } - } - } - .launchIn(lifecycleScope) - - attachmentController.isPanelVisible.listenValue { isVisible -> - if (isVisible) binding.message.setSelection(binding.message.text.toString().length) - -// val currentHeight = binding.listAnchor.height -// -// val newHeight = -// if (isVisible) (binding.attachmentPanel.measuredHeight / 1.5).roundToInt() -// else 1 -// -// ValueAnimator.ofInt(currentHeight, newHeight).apply { -// duration = ATTACHMENT_PANEL_ANIMATION_DURATION -// interpolator = LinearInterpolator() -// -// addUpdateListener { animator -> -// if (getView() == null) return@addUpdateListener -// val value = animator.animatedValue as Int -// -// binding.listAnchor.updateLayoutParams { -// height = value -// } -// } -// }.start() - } - - binding.replyMessage.setOnClickListener { - val message = attachmentController.message.value ?: return@setOnClickListener - val index = adapter.searchMessageIndex(message.id) ?: return@setOnClickListener - - binding.recyclerView.scrollToPosition(index) - } - - binding.dismissReply.setOnClickListener { - if (attachmentController.message.value != null) - attachmentController.message.update { null } - } - - binding.attach.setOnClickListener { - showAttachmentsPopupMenu() - } - - binding.attach.setOnLongClickListener { - pickPhoto() - true - } - } - - override fun onEvent(event: VkEvent) { - super.onEvent(event) - - when (event) { - is MessagesMarkAsImportantEvent -> markMessagesAsImportant(event) - is MessagesLoadedEvent -> refreshMessages(event) - is MessagesPinEvent -> conversation.pinnedMessage = event.message - is MessagesUnpinEvent -> conversation.pinnedMessage = null - is MessagesDeleteEvent -> deleteMessages(event) - is MessagesEditEvent -> editMessage(event) - is MessagesReadEvent -> readMessages(event) - is MessagesNewEvent -> addNewMessage(event) - } - } - - private fun checkIfNeedToScrollToBottom() { - if (adapter.isEmpty()) return - - val lastVisiblePosition = - (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() - - if (lastVisiblePosition <= adapter.lastPosition - 10) return - - binding.recyclerView.postDelayed({ - if (view == null) return@postDelayed - binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) - }, 0) - } - - private suspend fun processFileFromStorage(uri: Uri) { - var name = "" - var size = 0.0 - - val contentResolver = requireContext().contentResolver - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - - cursor.moveToFirst() - name = cursor.getString(nameIndex) - size = AndroidUtils.bytesToMegabytes(cursor.getLong(sizeIndex).toDouble()) - cursor.close() - } - - if (size > 200) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage("Selected file weighs more than 200 megabytes. Compress it or send other file") - .setPositiveButton(R.string.ok, null) - .setCancelable(false) - .show() - return - } - - val lastDotIndex = name.lastIndexOf(".") - var extension = if (lastDotIndex == -1) "" else name.substring(lastDotIndex + 1) - - if (extension.endsWith("msi") || extension.endsWith("exe") || extension.endsWith("apk")) { - extension += "fast" - name += "fast" - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage("Selected file is executable. Fast changed it extension to \"$extension\", so the final name is \"$name\"") - .setPositiveButton(R.string.ok, null) - .setCancelable(false) - .show() - } - - val destination = requireContext() - .getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + - "${File.separator}upload.$extension" - - val file = File(destination) - if (file.exists()) file.delete() - - withContext(Dispatchers.IO) { - val inputStream = - requireActivity().contentResolver.openInputStream(uri) ?: return@withContext - - inputStream.use { input -> - file.outputStream().use { output -> - input.copyTo(output) - } - } - } - - val mimeType = contentResolver.getType(uri) ?: return - - if (pickFile) { - val uploadedAttachment = viewModel.uploadFile( - conversation.id, - file, - name, - FilesRepository.FileType.File - ) - addAttachment(uploadedAttachment) - } else { - when (MediaType.parse(mimeType).type()) { - MediaType.ANY_IMAGE_TYPE.type() -> { - val uploadedAttachment = viewModel.uploadPhoto(conversation.id, file, name) - addAttachment(uploadedAttachment) - } - - MediaType.ANY_VIDEO_TYPE.type() -> { - val uploadedAttachment = viewModel.uploadVideo(file, name) - addAttachment(uploadedAttachment) - } - - MediaType.ANY_AUDIO_TYPE.type() -> { - val uploadedAttachment = viewModel.uploadAudio(file, name) - addAttachment(uploadedAttachment) - } - } - } - } - - private fun showAttachmentsPopupMenu() { - val popupMenu = PopupMenu(requireContext(), binding.attach) - - if (attachmentsToLoad.isNotEmpty()) { - popupMenu.menu.add("Clear attachments") - } - - popupMenu.menu.add("Photo") - popupMenu.menu.add("Video") - popupMenu.menu.add("Audio") - popupMenu.menu.add("File") - popupMenu.setOnMenuItemClickListener { menuItem -> - return@setOnMenuItemClickListener when (menuItem.title) { - "Clear attachments" -> { - clearAttachments() - true - } - - "Photo" -> { - pickPhoto() - true - } - - "Video" -> { - pickVideo() - true - } - - "Audio" -> { - pickAudio() - true - } - - "File" -> { - pickFile() - true - } - - else -> false - } - } - popupMenu.show() - } - - private fun addAttachment(attachment: VkAttachment) { - attachmentsToLoad += attachment - binding.attachmentsCounter.visible() - binding.attachmentsCounter.text = attachmentsToLoad.size.toString() - - binding.attachmentsList.visible() - attachmentsAdapter.add(attachment) - - attachmentController.showPanel() - - actionState.value = - if (attachmentController.isEditing) Action.EDIT - else Action.SEND - } - - private fun removeAttachment(attachment: VkAttachment) { - attachmentsToLoad -= attachment - binding.attachmentsCounter.visible() - binding.attachmentsCounter.text = attachmentsToLoad.size.toString() - - binding.attachmentsList.visible() - - attachmentController.showPanel() - - if (attachmentsToLoad.isEmpty()) { - clearAttachments() - } else { - attachmentsAdapter.remove(attachment) - } - } - - private fun clearAttachments() { - attachmentsToLoad.clear() - binding.attachmentsCounter.gone() - binding.attachmentsCounter.text = null - - attachmentsAdapter.clear() - binding.attachmentsList.gone() - - attachmentController.hidePanel() - } - - private fun pickPhoto() { - getContent.launch(MediaType.ANY_IMAGE_TYPE.mimeType) - } - - private fun pickVideo() { - getContent.launch(MediaType.ANY_VIDEO_TYPE.mimeType) - } - - private fun pickAudio() { - getContent.launch(MediaType.MPEG_AUDIO.mimeType) - } - - private fun pickFile() { - pickFile = true - getContent.launch(MediaType.ANY_TYPE.mimeType) - } - - fun scrollToMessage(messageId: Int) { - adapter.searchMessageIndex(messageId)?.let { index -> - binding.recyclerView.scrollToPosition(index) - } - } - - private fun prepareAvatar() { - val avatar = when { - conversation.isUser() -> user?.photo200 - conversation.isGroup() -> group?.photo200 - conversation.isChat() -> conversation.conversationPhoto - else -> null - } - - val avatarImageView = binding.toolbar.avatarImageView - avatarImageView.visible() - avatarImageView.loadWithGlide { - imageUrl = avatar - asCircle = true - crossFade = true - } - } - - private fun performAction() { - when (actionState.value) { - Action.RECORD -> { - sdk30AndUp { - binding.action.performHapticFeedback(HapticFeedbackConstants.REJECT) - } - } - - Action.SEND -> { - val messageText = binding.message.trimmedText - if (messageText.isBlank() && attachmentsToLoad.isEmpty()) { - Log.d( - "MessagesHistoryFragment", - "performAction: SEND: messageText is empty & attachments is empty. return" - ) - return - } - - val date = System.currentTimeMillis() - - val messageIndex = adapter.lastPosition - - val attachments = attachmentsToLoad.ifEmpty { null }?.toList() - clearAttachments() - - val message = VkMessage( - id = Int.MAX_VALUE, - text = messageText, - isOut = true, - peerId = conversation.id, - fromId = UserConfig.userId, - date = (date / 1000).toInt(), - randomId = Random.nextInt(), - replyMessage = attachmentController.message.value, - attachments = attachments, - ).also { - it.state = VkMessage.State.Sending - } - - Log.d("LongPollUpdatesParser", "newMessageRandomId: ${message.randomId}") - - adapter.add(message, commitCallback = { - binding.recyclerView.scrollToPosition(adapter.lastPosition) - binding.message.clear() - }) - - val replyMessage = attachmentController.message.value - attachmentController.message.update { null } - - sdk30AndUp { - binding.action.performHapticFeedback(HapticFeedbackConstants.CONFIRM) - } - - viewModel.sendMessage( - peerId = conversation.id, - message = messageText.ifBlank { null }, - randomId = message.randomId, - replyTo = replyMessage?.id, - setId = { messageId -> - val messageToUpdate = adapter[messageIndex] - messageToUpdate.id = messageId - messageToUpdate.state = VkMessage.State.Sent - adapter.notifyItemChanged(messageIndex, "kek") -// adapter[messageIndex] = messageToUpdate - attachmentsAdapter.clear() - }, - onError = { - val messageToUpdate = adapter[messageIndex] - messageToUpdate.state = VkMessage.State.Error - adapter.notifyItemChanged(messageIndex, "kek") -// adapter[messageIndex] = messageToUpdate - attachmentsAdapter.clear() - }, - attachments = attachments - ) - } - - Action.EDIT -> { - val message = attachmentController.message.value ?: return - val messageText = binding.message.text.toString().trim() - - attachmentController.message.update { null } - - viewModel.editMessage( - originalMessage = message, - peerId = conversation.id, - messageId = message.id, - message = messageText, - attachments = message.attachments - ) - } - - Action.DELETE -> attachmentController.message.value?.let { - showDeleteMessageDialog(it) - } - } - } - - private fun prepareViews() { - prepareRecyclerView() - prepareEmojiButton() - prepareAttachmentsList() - } - - private fun prepareRecyclerView() { - binding.recyclerView.itemAnimator = null - - binding.toolbar.measure( - View.MeasureSpec.AT_MOST, - View.MeasureSpec.UNSPECIFIED - ) - - binding.bottomMessagePanel.measure( - View.MeasureSpec.AT_MOST, - View.MeasureSpec.UNSPECIFIED - ) - -// binding.recyclerView.updatePaddingRelative( -// top = binding.toolbar.measuredHeight, -// bottom = binding.bottomMessagePanel.measuredHeight -// ) - - val toolbarMeasuredHeight = binding.toolbar.measuredHeight - val bottomMessagePanelMeasuredHeight = binding.bottomMessagePanel.measuredHeight - - binding.recyclerView.doOnApplyWindowInsets { v, insets, _, _ -> - val statusBars = AndroidUtils.getStatusBarInsets(insets) - val ime = AndroidUtils.getImeInsets(insets) - val navBars = AndroidUtils.getNavBarInsets(insets) - - val topPadding = toolbarMeasuredHeight + statusBars.top - - val bottomPadding = bottomMessagePanelMeasuredHeight + - ime.bottom + (if (ime.bottom == 0) navBars.bottom else 0) - - val currentPadding = v.paddingBottom - - v.updatePaddingRelative(top = topPadding) - ValueAnimator.ofInt(currentPadding, bottomPadding).apply { - interpolator = LinearInterpolator() - duration = if (currentPadding > bottomPadding) 125 else 50 - - addUpdateListener { - if (view == null) return@addUpdateListener - val value = it.animatedValue as Int - v.updatePaddingRelative(bottom = value) - } - - doOnEnd { - if (view == null) return@doOnEnd - checkIfNeedToScrollToBottom() - } - }.start() - -// v.updatePaddingRelative(top = topPadding, bottom = bottomPadding) - - insets - } - } - - private fun prepareEmojiButton() { - binding.emoji.setOnClickListener { - sdk30AndUp { - binding.emoji.performHapticFeedback(HapticFeedbackConstants.REJECT) - } - } - binding.emoji.setOnLongClickListener { - val text = binding.message.text.toString() + - AppGlobal.preferences.getString( - SettingsFragment.KEY_FEATURES_FAST_TEXT, - SettingsFragment.DEFAULT_VALUE_FEATURES_FAST_TEXT - ) - binding.message.setText(text) - binding.message.selectLast() - - binding.emoji.animate() - .scaleX(1.25f) - .scaleY(1.25f) - .setDuration(100) - .withEndAction { - if (view == null) return@withEndAction - - binding.emoji.animate() - .scaleX(1f) - .scaleY(1f) - .setDuration(100) - .start() - }.start() - - binding.emoji.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - true - } - } - - private fun prepareAttachmentsList() { - binding.attachmentsList.addItemDecoration( - SpaceItemDecoration(endMargin = 4.dpToPx()) - ) - binding.attachmentsList.adapter = attachmentsAdapter - } - - private fun markMessagesAsImportant(event: MessagesMarkAsImportantEvent) { - val newList = adapter.cloneCurrentList() - - for (i in newList.indices) { - val message = newList[i] - if (event.messagesIds.contains(message.id)) { - newList[i] = message.copy(important = event.important) - } - } - - adapter.submitList(newList) - } - - private fun refreshMessages(event: MessagesLoadedEvent) { - adapter.profiles += event.profiles - adapter.groups += event.groups - - fillRecyclerView(event.messages) - } - - private fun fillRecyclerView(values: List) { - val smoothScroll = adapter.isNotEmpty() - - adapter.setItems( - values.sortedBy { it.date }, - commitCallback = { - if (view == null) return@setItems - if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) - else binding.recyclerView.scrollToPosition(adapter.lastPosition) - } - ) - } - - private fun onItemClick(position: Int) { - showOptionsDialog(position) - } - - private fun onAvatarLongClickListener(position: Int) { - val message = adapter[position] - - val messageUser = VkUtils.getMessageUser(message, adapter.profiles) - val messageGroup = VkUtils.getMessageGroup(message, adapter.groups) - - val title = VkUtils.getMessageTitle(message, messageUser, messageGroup) - Toast.makeText(requireContext(), title, Toast.LENGTH_SHORT).show() - } - - private fun showOptionsDialog(position: Int) { - val message = adapter[position] - if (message.action != null) return - - val time = getString( - R.string.time_format, - SimpleDateFormat( - "dd.MM.yyyy, HH:mm:ss", - Locale.getDefault() - ).format(message.date * 1000L) - ) - - val reply = getString(R.string.message_context_action_reply) - - val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id - - val pin = getString( - if (isMessageAlreadyPinned) R.string.message_context_action_unpin - else R.string.message_context_action_pin - ) - - val important = getString( - if (message.important) R.string.message_context_action_unmark_as_important - else R.string.message_context_action_mark_as_important - ) - - val read = "Mark as read" - - val edit = getString(R.string.message_context_action_edit) - - val copy = "Copy" - - val share = "Share" - - val delete = getString(R.string.message_context_action_delete) - - val params = mutableListOf() - val onlySentParams = mutableListOf() - - params += reply - onlySentParams += reply - - if (conversation.canChangePin) { - params += pin - onlySentParams += pin - } - - params += important - onlySentParams += important - - if (!message.isRead(conversation) && !message.isOut) { - params += read - onlySentParams += read - } - - if (message.canEdit()) { - params += edit - onlySentParams += edit - } - - val notNullText = message.text.orEmpty() - val messageTextIsNotNull = !message.text.isNullOrBlank() - - val notNullAttachments = message.attachments.orEmpty() - val attachmentsIsOnePhoto = notNullAttachments.size == 1 && - notNullAttachments.first() is VkPhoto - - if (messageTextIsNotNull || attachmentsIsOnePhoto) { - params += copy - params += share - } - - params += delete - - if (!message.isSent()) { - params.removeAll(onlySentParams) - } - - val arrayParams = params.toTypedArray() - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(time) - .setItems(arrayParams) { _, which -> - when (params[which]) { - reply -> { - if (attachmentController.message.value != message) - attachmentController.message.update { message } - } - - pin -> showPinMessageDialog( - peerId = conversation.id, - messageId = message.id, - pin = !isMessageAlreadyPinned - ) - - important -> viewModel.markAsImportant( - messagesIds = listOf(message.id), - important = !message.important - ) - - read -> viewModel.readMessage( - conversation.id, - message.id - ) - - edit -> { - attachmentController.isEditing = true - - if (attachmentController.message.value != message) - attachmentController.message.update { message } - } - - copy -> { - lifecycleScope.launch(Dispatchers.IO) { - when { - messageTextIsNotNull && !attachmentsIsOnePhoto -> { - withContext(Dispatchers.Main) { - AndroidUtils.copyText( - text = notNullText, - withToast = true - ) - } - } - - else -> { - val imageUrl = - ((notNullAttachments.first() as? VkPhoto)?.getMaxSize() - ?: return@launch).url - - val preloadedImageFileUri = Glide - .with(requireContext()) - .downloadOnly() - .load(imageUrl) - .submit() - .get().let { file -> - val newFile = - AndroidUtils.getImageToShare(requireContext(), file) - - newFile!! - } - - withContext(Dispatchers.Main) { - if (messageTextIsNotNull) { - AndroidUtils.copyText(text = notNullText) - AndroidUtils.copyImage( - label = "Image", - imageUri = preloadedImageFileUri, - withToast = true - ) - } else { - AndroidUtils.copyImage( - label = "Image", - imageUri = preloadedImageFileUri, - withToast = true - ) - } - } - } - } - } - } - - share -> { - lifecycleScope.launch(Dispatchers.IO) { - val content = when { - messageTextIsNotNull && !attachmentsIsOnePhoto -> { - ShareContent.Text(notNullText) - } - - else -> { - val imageUrl = - ((notNullAttachments.first() as? VkPhoto)?.getMaxSize() - ?: return@launch).url - - val preloadedImageFileUri = Glide - .with(requireContext()) - .downloadOnly() - .load(imageUrl) - .submit() - .get().let { file -> - val newFile = - AndroidUtils.getImageToShare(requireContext(), file) - - newFile!! - } - - if (messageTextIsNotNull) { - ShareContent.TextWithImage( - notNullText, preloadedImageFileUri - ) - } else { - ShareContent.Image(preloadedImageFileUri) - } - } - } - - withContext(Dispatchers.Main) { - AndroidUtils.showShareSheet(requireActivity(), content) - } - } - } - - delete -> showDeleteMessageDialog(message) - } - }.show() - } - - private fun showPinMessageDialog( - peerId: Int, - messageId: Int?, - pin: Boolean, - ) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle( - if (pin) R.string.confirm_pin_message - else R.string.confirm_unpin_message - ) - .setPositiveButton( - if (pin) R.string.action_pin - else R.string.action_unpin - ) { _, _ -> - viewModel.pinMessage( - peerId = peerId, - messageId = messageId, - pin = pin - ) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - - private fun showDeleteMessageDialog(message: VkMessage) { - val binding = DialogMessageDeleteBinding.inflate(layoutInflater, null, false) - - binding.check.setText( - if (message.isOut || conversation.canChangeInfo) R.string.message_delete_for_all - else R.string.message_mark_as_spam - ) - - binding.check.isEnabled = - message.isSent() && ((conversation.id != UserConfig.userId) && (!message.isOut || message.canEdit())) - - if (message.isSent() && conversation.id == UserConfig.userId || - (binding.check.isEnabled && message.isOut) - ) binding.check.isChecked = true - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.confirm_delete_message) - .setView(binding.root) - .setPositiveButton(R.string.action_delete) { _, _ -> - attachmentController.message.update { null } - - if (message.isError()) { - adapter.searchIndexOf(message)?.let { index -> - adapter.removeAt(index) - } - - return@setPositiveButton - } - - viewModel.deleteMessage( - peerId = conversation.id, - messagesIds = listOf(message.id), - isSpam = if (message.isOut) null else binding.check.isChecked, - deleteForAll = if (!binding.check.isEnabled) null else binding.check.isChecked - ) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - - private fun deleteMessages(event: MessagesDeleteEvent) { - if (event.peerId != conversation.id) return - val messagesToDelete = - event.messagesIds.mapNotNull { id -> adapter.searchMessageById(id) } - adapter.removeAll(messagesToDelete) - } - - private fun editMessage(event: MessagesEditEvent) { - if (event.message.peerId != conversation.id) return - adapter.searchMessageIndex(event.message.id)?.let { index -> - adapter[index] = event.message - adapter.notifyItemChanged(index) - } - } - - private fun readMessages(event: MessagesReadEvent) { - if (event.peerId != conversation.id) return - - val oldOutRead = conversation.outRead - val oldInRead = conversation.inRead - - conversation = conversation.copy( - outRead = if (event.isOut) event.messageId else conversation.outRead, - inRead = if (!event.isOut) event.messageId else conversation.inRead - ) - - val positionsToUpdate = mutableListOf() - val newList = adapter.cloneCurrentList() - for (i in newList.indices) { - val message = newList[i] - - if ((message.isOut && conversation.outRead - oldOutRead > 0 && message.id > oldOutRead) || - (!message.isOut && conversation.inRead - oldInRead > 0 && message.id > oldInRead) - ) { - positionsToUpdate += i - } - } - - positionsToUpdate.forEach { index -> - adapter.notifyItemChanged(index) - - if (binding.unreadCounter.isVisible) { - setUnreadCounterVisibility( - (binding.recyclerView.layoutManager as LinearLayoutManager) - .findLastCompletelyVisibleItemPosition() - ) - } - } - } - - private fun setUnreadCounterVisibility( - lastCompletelyVisiblePosition: Int, - dy: Int? = null, - ) { - if (lastCompletelyVisiblePosition >= adapter.lastPosition - 1) { - setUnreadCounterVisibility(false) - } else { - if (adapter.containsUnreadMessages()) { - setUnreadCounterVisibility(true) - } else { - if (dy == null) { - setUnreadCounterVisibility(false) - } else { - if (dy > 0) { - if (dy > 40) setUnreadCounterVisibility(true) - } else { - if (dy < -40) setUnreadCounterVisibility(false) - } - } - } - } - } - - private fun addNewMessage(event: MessagesNewEvent) { - if (event.message.peerId != conversation.id) return - - adapter.profiles += event.profiles - adapter.groups += event.groups - - if (adapter.containsRandomId(event.message.randomId) - || adapter.containsId(event.message.id) - ) return - - val itemCount = adapter.itemCount - - adapter.add(event.message) { - if (view == null) return@add - - val lastVisiblePosition = - (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() - - if (abs(lastVisiblePosition - adapter.lastPosition) <= 3) { - binding.recyclerView.scrollToPosition(adapter.lastPosition) - } else { - setUnreadCounterVisibility(true) - // add counter of unread - } - } - adapter.notifyItemRangeChanged(0, itemCount, "avatars") - } - - private fun setUnreadCounterVisibility(isVisible: Boolean) { - if (view == null) return - - binding.unreadCounter.run { - if (isVisible) { - show() - } else { - hide() - } - } - } - - private class AttachmentPanelController { - companion object { - fun init( - context: Context, - adapter: MessagesHistoryAdapter, - lifecycleOwner: LifecycleOwner, - binding: FragmentMessagesHistoryBinding, - isAttachmentsEmpty: () -> Boolean, - ): AttachmentPanelController { - val controller = AttachmentPanelController().apply { - this.context = context - this.binding = binding - this.adapter = adapter - this.isAttachmentsEmpty = isAttachmentsEmpty - this.message.listenValue( - lifecycleOwner.lifecycleScope, - this::onMessageValueChanged - ) - - this.message.update { null } - } - - - return controller - } - } - - val isPanelVisible = MutableStateFlow(false) - val message = MutableStateFlow(null) - - var isEditing = false - - var adapter: MessagesHistoryAdapter by Delegates.notNull() - var binding: FragmentMessagesHistoryBinding by Delegates.notNull() - var context: Context by Delegates.notNull() - var isAttachmentsEmpty: () -> Boolean by Delegates.notNull() - - fun onMessageValueChanged(value: VkMessage?) { - if (value != null) { - applyMessage(value) - } else { - clearMessage() - } - } - - private fun applyMessage(message: VkMessage) { - val messageUser: VkUser? = - if (message.isUser()) adapter.profiles[message.fromId] - else null - val messageGroup: VkGroup? = - if (message.isGroup()) adapter.groups[message.fromId] - else null - val title = VkUtils.getMessageTitle( - message, messageUser, messageGroup - ) - - val attachmentText = (if (message.text == null) VkUtils.getAttachmentText( - message = message - ) else null)?.parseString(context) - - val forwardsMessage = (if (message.text == null) VkUtils.getForwardsText( - message = message - ) else null)?.parseString(context) - - val messageText = forwardsMessage ?: attachmentText - ?: (message.text ?: "").run { VkUtils.prepareMessageText(this) } - - binding.replyMessageTitle.text = title - binding.replyMessageText.text = messageText - - if (isEditing) { - binding.message.setText(message.text) - binding.message.setSelection(message.text?.length ?: 0) - binding.message.requestFocusFromTouch() - binding.message.showKeyboard() - } - - binding.replyMessage.visible() - - showPanel() - } - - private fun clearMessage() { - if (isAttachmentsEmpty()) { - hidePanel() - } - - binding.replyMessage.gone() - - binding.replyMessageTitle.clear() - binding.replyMessageText.clear() - - if (isEditing) { - isEditing = false - binding.message.clear() - } - } - - fun showPanel() { - if (isPanelVisible.value) return - - binding.attachmentPanel.visible() -// binding.attachmentPanel.measure( -// View.MeasureSpec.AT_MOST, View.MeasureSpec.UNSPECIFIED -// ) - - if (!isPanelVisible.value) - isPanelVisible.update { true } - -// binding.attachmentPanel.visible() - -// val measuredHeight = binding.attachmentPanel.measuredHeight -// -// binding.attachmentPanel.updateLayoutParams { -// height = 0 -// } -// -// binding.attachmentPanel.animate() -// .translationY(0f) -// .setDuration(ATTACHMENT_PANEL_ANIMATION_DURATION) -// .start() -// -// ValueAnimator.ofInt(0, measuredHeight).apply { -// duration = ATTACHMENT_PANEL_ANIMATION_DURATION -// interpolator = LinearInterpolator() -// -// addUpdateListener { animator -> -// if (view == null) return@addUpdateListener -// val value = animator.animatedValue as Int -// -// if (value >= 36.dpToPx()) { -// binding.attachmentPanel.visible() -// } -// -// binding.attachmentPanel.updateLayoutParams { -// height = value -// } -// } -// }.start() - } - - fun hidePanel() { - if (!isPanelVisible.value || - !isAttachmentsEmpty() || - message.value != null - ) return - - if (isPanelVisible.value) - isPanelVisible.update { false } - - binding.attachmentPanel.gone() - -// val currentHeight = binding.attachmentPanel.height -// -// binding.attachmentPanel.animate() -// .translationY(75F) -// .setDuration(ATTACHMENT_PANEL_ANIMATION_DURATION) -// .start() -// -// ValueAnimator.ofInt(currentHeight, 0).apply { -// duration = ATTACHMENT_PANEL_ANIMATION_DURATION -// interpolator = LinearInterpolator() -// -// addUpdateListener { animator -> -// if (view == null) return@addUpdateListener -// val value = animator.animatedValue as Int -// -// if (value <= 36.dpToPx()) { -// binding.attachmentPanel.gone() -// } -// -// binding.attachmentPanel.updateLayoutParams { -// height = value -// } -// } -// doOnEnd { -// if (view == null) return@doOnEnd -// binding.attachmentPanel.gone() -// } -// }.start() - } - } - - fun openForwardsScreen( - conversation: VkConversationDomain, - messages: List, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf(), - ) { - router.navigateTo( - Screens.ForwardedMessages(conversation, messages, profiles, groups) - ) - } - - private fun openChatInfoScreen( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup?, - ) { - router.navigateTo( - Screens.ChatInfo(conversation, user, group) - ) - } - - companion object { - const val ARG_USER: String = "user" - const val ARG_GROUP: String = "group" - const val ARG_CONVERSATION: String = "conversation" - - private const val ATTACHMENT_PANEL_ANIMATION_DURATION = 150L - - fun newInstance( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup?, - ): MessagesHistoryFragment { - val fragment = MessagesHistoryFragment() - fragment.arguments = bundleOf( - ARG_CONVERSATION to conversation, - ARG_USER to user, - ARG_GROUP to group - ) - - return fragment - } - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt deleted file mode 100644 index ebc05fce..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt +++ /dev/null @@ -1,598 +0,0 @@ -package com.meloda.fast.screens.messages - -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.longpoll.LongPollEvent -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -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.attachments.VkAttachment -import com.meloda.fast.api.model.attachments.VkVideo -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.network.messages.MessagesDeleteRequest -import com.meloda.fast.api.network.messages.MessagesEditRequest -import com.meloda.fast.api.network.messages.MessagesGetHistoryRequest -import com.meloda.fast.api.network.messages.MessagesMarkAsImportantRequest -import com.meloda.fast.api.network.messages.MessagesPinMessageRequest -import com.meloda.fast.api.network.messages.MessagesSendRequest -import com.meloda.fast.api.network.messages.MessagesUnPinMessageRequest -import com.meloda.fast.api.network.photos.PhotosSaveMessagePhotoRequest -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.data.audios.AudiosRepository -import com.meloda.fast.data.files.FilesRepository -import com.meloda.fast.data.messages.MessagesRepository -import com.meloda.fast.data.photos.PhotosRepository -import com.meloda.fast.data.videos.VideosRepository -import com.meloda.fast.ext.notNull -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody -import java.io.File -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -class MessagesHistoryViewModel constructor( - private val messagesRepository: MessagesRepository, - updatesParser: LongPollUpdatesParser, - private val photosRepository: PhotosRepository, - private val filesRepository: FilesRepository, - private val audiosRepository: AudiosRepository, - private val videosRepository: VideosRepository, -) : DeprecatedBaseViewModel() { - - init { - updatesParser.onNewMessage { - launch { handleNewMessage(it) } - } - - updatesParser.onMessageEdited { - launch { handleEditedMessage(it) } - } - - updatesParser.onMessageIncomingRead { - launch { handleReadIncomingEvent(it) } - } - - updatesParser.onMessageOutgoingRead { - launch { handleReadOutgoingEvent(it) } - } - } - - private suspend fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { - sendEvent(MessagesNewEvent(event.message, event.profiles, event.groups)) - } - - private suspend fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) { - sendEvent(MessagesEditEvent(event.message)) - } - - private suspend fun handleReadIncomingEvent(event: LongPollEvent.VkMessageReadIncomingEvent) { - sendEvent( - MessagesReadEvent( - isOut = false, - peerId = event.peerId, - messageId = event.messageId - ) - ) - } - - private suspend fun handleReadOutgoingEvent(event: LongPollEvent.VkMessageReadOutgoingEvent) { - sendEvent( - MessagesReadEvent( - isOut = true, - peerId = event.peerId, - messageId = event.messageId - ) - ) - } - - fun loadHistory(peerId: Int) = launch { - makeJob({ - messagesRepository.getHistory( - MessagesGetHistoryRequest( - count = 100, - peerId = peerId, - extended = true, - fields = VKConstants.ALL_FIELDS - ) - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - - val profiles = hashMapOf() - response.profiles?.let { baseProfiles -> - baseProfiles.forEach { baseProfile -> - baseProfile.mapToDomain().let { profile -> profiles[profile.id] = profile } - } - } - - val groups = hashMapOf() - response.groups?.let { baseGroups -> - baseGroups.forEach { baseGroup -> - baseGroup.mapToDomain().let { group -> groups[group.id] = group } - } - } - - val hashMessages = hashMapOf() - response.items.forEach { baseMessage -> - baseMessage.asVkMessage() - .let { message -> hashMessages[message.id] = message } - } - - messagesRepository.store(hashMessages.values.toList()) - - val conversations = hashMapOf() - response.conversations?.let { baseConversations -> - baseConversations.forEach { baseConversation -> - baseConversation.mapToDomain( - hashMessages[baseConversation.last_message_id] - ).let { conversation -> conversations[conversation.id] = conversation } - } - } - - sendEvent( - MessagesLoadedEvent( - count = response.count, - profiles = profiles, - groups = groups, - conversations = conversations, - messages = hashMessages.values.toList() - ) - ) - }) - } - - fun sendMessage( - peerId: Int, - message: String? = null, - randomId: Int = 0, - replyTo: Int? = null, - setId: ((messageId: Int) -> Unit)? = null, - onError: ((error: Throwable) -> Unit)? = null, - attachments: List? = null, - ) = launch { - makeJob( - { - messagesRepository.send( - MessagesSendRequest( - peerId = peerId, - randomId = randomId, - message = message, - replyTo = replyTo, - attachments = attachments - ) - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - setId?.invoke(response) - }, - onError = { - onError?.invoke(it) - }) - } - - fun markAsImportant( - messagesIds: List, - important: Boolean, - ) = launch { - makeJob({ - messagesRepository.markAsImportant( - MessagesMarkAsImportantRequest( - messagesIds = messagesIds, - important = important - ) - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - sendEvent( - MessagesMarkAsImportantEvent( - messagesIds = response, - important = important - ) - ) - }) - } - - fun pinMessage( - peerId: Int, - messageId: Int? = null, - conversationMessageId: Int? = null, - pin: Boolean, - ) = launch { - if (pin) { - makeJob({ - messagesRepository.pin( - MessagesPinMessageRequest( - peerId = peerId, - messageId = messageId, - conversationMessageId = conversationMessageId - ) - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - sendEvent(MessagesPinEvent(response.asVkMessage())) - } - ) - } else { - makeJob({ messagesRepository.unpin(MessagesUnPinMessageRequest(peerId = peerId)) }, - onAnswer = { - println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}") - sendEvent(MessagesUnpinEvent) - } - ) - } - } - - fun deleteMessage( - peerId: Int, - messagesIds: List? = null, - conversationsMessagesIds: List? = null, - isSpam: Boolean? = null, - deleteForAll: Boolean? = null, - ) = launch { - makeJob( - { - messagesRepository.delete( - MessagesDeleteRequest( - peerId = peerId, - messagesIds = messagesIds, - conversationsMessagesIds = conversationsMessagesIds, - isSpam = isSpam, - deleteForAll = deleteForAll - ) - ) - }, - onAnswer = { - sendEvent( - MessagesDeleteEvent( - peerId = peerId, - messagesIds = messagesIds ?: emptyList() - ) - ) - }) - } - - fun editMessage( - originalMessage: VkMessage, - peerId: Int, - messageId: Int, - message: String? = null, - attachments: List? = null, - ) = launch { - makeJob( - { - messagesRepository.edit( - MessagesEditRequest( - peerId = peerId, - messageId = messageId, - message = message, - attachments = attachments - ) - ) - }, - onAnswer = { - originalMessage.text = message - sendEvent(MessagesEditEvent(originalMessage)) - } - ) - } - - fun readMessage(peerId: Int, messageId: Int) { - makeJob( - { messagesRepository.markAsRead(peerId, startMessageId = messageId) }, - onAnswer = { - sendEvent(MessagesReadEvent(false, peerId, messageId)) - } - ) - } - - suspend fun uploadPhoto( - peerId: Int, - photo: File, - name: String, - ) = suspendCoroutine { - launch { - val uploadServerUrl = getPhotoMessageUploadServer(peerId) - val uploadedFileInfo = uploadPhotoToServer(uploadServerUrl, photo, name) - - val savedAttachment = saveMessagePhoto( - uploadedFileInfo.first, - uploadedFileInfo.second, - uploadedFileInfo.third - ) - - it.resume(savedAttachment) - } - } - - private suspend fun getPhotoMessageUploadServer(peerId: Int) = - suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { photosRepository.getMessagesUploadServer(peerId) } - ).response?.let { response -> - continuation.resume(response.uploadUrl) - } - } - } - - private suspend fun uploadPhotoToServer( - uploadUrl: String, - photo: File, - name: String, - ) = suspendCoroutine { continuation -> - launch { - val requestBody = photo.asRequestBody("image/*".toMediaType()) - val body = MultipartBody.Part.createFormData("photo", name, requestBody) - - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { photosRepository.uploadPhoto(uploadUrl, body) } - ).let { response -> - continuation.resume(Triple(response.server, response.photo, response.hash)) - } - } - } - - private suspend fun saveMessagePhoto( - server: Int, - photo: String, - hash: String, - ) = suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { - photosRepository.saveMessagePhoto( - PhotosSaveMessagePhotoRequest(photo, server, hash) - ) - } - ).response?.first()?.asVkPhoto()?.let(continuation::resume) - } - } - - suspend fun uploadVideo( - file: File, - name: String, - ) = suspendCoroutine { - launch { - val uploadInfo = getVideoMessageUploadServer() - - uploadVideoToServer( - uploadInfo.first, - file, - name - ) - - it.resume(uploadInfo.second) - } - } - - private suspend fun getVideoMessageUploadServer() = suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { videosRepository.save() } - ).response?.let { response -> - val uploadUrl = response.uploadUrl - val video = VkVideo( - id = response.videoId, - ownerId = response.ownerId, - images = emptyList(), - firstFrames = null, - accessKey = response.accessKey, - title = response.title - ) - - continuation.resume(uploadUrl to video) - } - } - } - - private suspend fun uploadVideoToServer( - uploadUrl: String, - file: File, - name: String, - ) = launch { - val requestBody = file.asRequestBody() - val body = MultipartBody.Part.createFormData("video_file", name, requestBody) - - sendRequest( - onError = { exception -> throw exception }, - request = { videosRepository.upload(uploadUrl, body) } - ) - } - - suspend fun uploadAudio( - file: File, - name: String, - ) = suspendCoroutine { - launch { - val uploadUrl = getAudioUploadServer() - val uploadInfo = uploadAudioToServer(uploadUrl, file, name) - val saveInfo = saveMessageAudio( - uploadInfo.first, uploadInfo.second, uploadInfo.third - ) - - it.resume(saveInfo) - } - } - - private suspend fun getAudioUploadServer() = suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { audiosRepository.getUploadServer() } - ).response?.uploadUrl?.let(continuation::resume) - } - } - - private suspend fun uploadAudioToServer( - uploadUrl: String, - file: File, - name: String, - ) = suspendCoroutine { continuation -> - launch { - val requestBody = file.asRequestBody() - val body = MultipartBody.Part.createFormData("file", name, requestBody) - - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { audiosRepository.upload(uploadUrl, body) } - ).let { response -> - response.error?.let { error -> throw ApiError(error = error) } - - continuation.resume( - Triple(response.server, response.audio.notNull(), response.hash) - ) - } - } - } - - private suspend fun saveMessageAudio( - server: Int, - audio: String, - hash: String, - ) = suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { audiosRepository.save(server, audio, hash) } - ).response?.asVkAudio()?.let(continuation::resume) - } - } - - suspend fun uploadFile( - peerId: Int, - file: File, - name: String, - type: FilesRepository.FileType, - ) = suspendCoroutine { continuation -> - launch { - val uploadServerUrl = getFileMessageUploadServer(peerId, type) - val uploadedFileInfo = uploadFileToServer(uploadServerUrl, file, name) - val savedAttachmentPair = saveMessageFile(uploadedFileInfo) - - continuation.resume(savedAttachmentPair.second) - } - } - - private suspend fun getFileMessageUploadServer( - peerId: Int, - type: FilesRepository.FileType, - ) = suspendCoroutine { continuation -> - launch { - val uploadServerResponse = sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { filesRepository.getMessagesUploadServer(peerId, type) } - ).response.notNull() - - continuation.resume(uploadServerResponse.uploadUrl) - } - } - - private suspend fun uploadFileToServer( - uploadUrl: String, - file: File, - name: String, - ) = suspendCoroutine { continuation -> - launch { - val requestBody = file.asRequestBody() - val body = MultipartBody.Part.createFormData("file", name, requestBody) - - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { filesRepository.uploadFile(uploadUrl, body) } - ).let { response -> - response.error?.let { error -> throw ApiError(error = error) } - - continuation.resume(response.file.notNull()) - } - } - } - - private suspend fun saveMessageFile(file: String) = - suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { filesRepository.saveMessageFile(file) } - ).response?.let { response -> - val type = response.type - val attachmentFile = - response.file?.asVkFile() ?: response.voiceMessage?.asVkVoiceMessage() - - continuation.resume(type to attachmentFile.notNull()) - } - } - } -} - -data class MessagesLoadedEvent( - val count: Int, - val conversations: HashMap, - val messages: List, - val profiles: HashMap, - val groups: HashMap, -) : VkEvent() - -data class MessagesMarkAsImportantEvent(val messagesIds: List, val important: Boolean) : - VkEvent() - -data class MessagesPinEvent(val message: VkMessage) : VkEvent() - -object MessagesUnpinEvent : VkEvent() - -data class MessagesDeleteEvent(val peerId: Int, val messagesIds: List) : VkEvent() - -data class MessagesEditEvent(val message: VkMessage) : VkEvent() - -data class MessagesReadEvent( - val isOut: Boolean, - val peerId: Int, - val messageId: Int, -) : VkEvent() - -data class MessagesNewEvent( - val message: VkMessage, - val profiles: HashMap, - val groups: HashMap, -) : VkEvent() diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt deleted file mode 100644 index 74bfb33e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt +++ /dev/null @@ -1,303 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.drawable.ColorDrawable -import android.text.method.LinkMovementMethod -import android.util.Log -import android.view.View -import android.widget.* -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import com.meloda.fast.R -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.domain.VkConversationDomain -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.common.AppGlobal -import com.meloda.fast.ext.clear -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.orDots -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.visible -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import java.text.SimpleDateFormat -import java.util.* - - -class MessagesPreparator constructor( - private val context: Context, - - private val position: Int, - - private val adapterClickListener: ((position: Int) -> Unit)? = null, - - private val payloads: MutableList? = null, - - private val root: View? = null, - - private val conversation: VkConversationDomain, - private val message: VkMessage, - private val prevMessage: VkMessage? = null, - private val nextMessage: VkMessage? = null, - - private val bubble: ConstraintLayout, - private val text: TextView? = null, - private val avatar: ImageView? = null, - private val title: TextView? = null, - private val spacer: Space? = null, - private val messageState: ImageView? = null, - private val time: TextView? = null, - private val replyContainer: FrameLayout? = null, - private val timeReadContainer: View, - private val attachmentContainer: LinearLayoutCompat? = null, - - private val profiles: Map, - private val groups: Map, - - private val isForwards: Boolean = false -) { - - private val rootHighlightedColor = - ContextCompat.getColor(context, R.color.n2_100) - - private val mentionColor = - ContextCompat.getColor(context, R.color.colorPrimary) - - private var photoClickListener: ((url: String) -> Unit)? = null - private var replyClickListener: ((replyMessage: VkMessage) -> Unit)? = null - private var forwardsClickListener: ((forwards: List) -> Unit)? = null - - fun withPhotoClickListener(block: ((url: String) -> Unit)?): MessagesPreparator { - this.photoClickListener = block - return this - } - - fun withReplyClickListener(block: ((replyMessage: VkMessage) -> Unit)?): MessagesPreparator { - this.replyClickListener = block - return this - } - - fun withForwardsClickListener(block: ((forwards: List) -> Unit)?): MessagesPreparator { - this.forwardsClickListener = block - return this - } - - fun prepare() { - val messageUser = VkUtils.getMessageUser(message, profiles) - val messageGroup = VkUtils.getMessageGroup(message, groups) - - prepareRootBackground() - - prepareTime() - - prepareUnreadIndicator() - - prepareSpacer() - - prepareAttachments() - - prepareBubbleBackground() - - prepareText() - - prepareAvatar( - messageUser = messageUser, - messageGroup = messageGroup - ) - - if (message.isPeerChat() || isForwards) { - val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message) - val nextSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(message, nextMessage) - val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) - val nextMessageFiveMinAfter = - VkUtils.isPreviousMessageSentFiveMinutesAgo(message, nextMessage) - - val change = (prevMessage?.date ?: 0) - message.date - - Log.d( - "Fast::MessagesPreparator", - "text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $prevSenderDiff; fiveMinAgo: $fiveMinAgo; " - ) - - title?.toggleVisibility(prevSenderDiff || fiveMinAgo) - - avatar?.visibility = - if (nextSenderDiff - || (fiveMinAgo && prevSenderDiff && nextMessageFiveMinAfter) - || nextMessageFiveMinAfter - || (!prevSenderDiff && nextSenderDiff) - || nextMessage == null - ) View.VISIBLE else View.INVISIBLE - } else { - title?.gone() - avatar?.gone() - } - - - bubble.run { - updateLayoutParams { - matchConstraintMaxWidth = - if (avatar?.isVisible == true) AppGlobal.screenWidth80 - avatar.width - 6.dpToPx() - else AppGlobal.screenWidth80 - } - } - - if (title != null) { - val titleString = when { - message.isUser() && messageUser != null -> messageUser.fullName - message.isGroup() && messageGroup != null -> messageGroup.name - else -> null - } - - title.text = titleString.orDots() - } - } - - private fun prepareRootBackground() { - if (root != null) { - root.background = - if (message.isSelected) ColorDrawable(rootHighlightedColor) - else null - } - } - - private fun prepareTime() { - val sentTime = SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L) - - val timeText = - if (message.isUpdated()) context.getString(R.string.message_update_time_short, sentTime) - else sentTime - - time?.text = timeText - } - - private fun prepareUnreadIndicator() { - val isMessageRead = message.isRead(conversation) - - val drawableRes: Int = when (message.state) { - VkMessage.State.Sending -> { - R.drawable.ic_round_access_time_24 - } - VkMessage.State.Error -> { - R.drawable.ic_round_error_outline_24 - } - VkMessage.State.Sent -> { - if (isMessageRead) R.drawable.ic_round_done_all_24 - else R.drawable.ic_round_done_24 - } - } - - messageState?.run { - imageTintList = ColorStateList.valueOf( - ContextCompat.getColor( - context, - if (message.state == VkMessage.State.Error) R.color.colorError - else R.color.colorOnBackground - ) - ) - - toggleVisibility(!isMessageRead || message.isOut) - setImageResource(drawableRes) - } - } - - private fun prepareSpacer() { - val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) - val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message) - spacer?.toggleVisibility(fiveMinAgo || prevSenderDiff) - } - - private fun prepareAttachments() { - attachmentContainer?.removeAllViews() - - if (attachmentContainer != null && replyContainer != null) { - if ( - !message.hasAttachments() && - !message.hasReply() && - !message.hasForwards() && - !message.hasGeo() - ) { - attachmentContainer.gone() - replyContainer.gone() - } else { - AttachmentInflater( - context = context, - container = attachmentContainer, - replyContainer = replyContainer, - timeReadContainer = timeReadContainer, - message = message, - groups = groups, - profiles = profiles - ) - .withPhotoClickListener(photoClickListener) - .withReplyClickListener(replyClickListener) - .withForwardsClickListener(forwardsClickListener) - .inflate() - } - } - } - - private fun prepareBubbleBackground() { -// bubble.background = if (message.isOut) backgroundMiddleOut else backgroundMiddleIn - } - - private fun prepareText() { - if (text != null) { - text.setOnClickListener { adapterClickListener?.invoke(position) } - text.movementMethod = LinkMovementMethod.getInstance() - text.updateLayoutParams { - val topMargin = (if (title != null && title.isVisible) 6 else 0).dpToPx() - - goneTopMargin = topMargin - } - - if (message.text == null) { - text.clear() - text.gone() - } else { - text.visible() - - val updSpacer = "\t\t\t\t" - val timeSpacer = "\t\t\t\t\t\t" - val messageStateSpacer = "\t\t\t" - - val preparedText = - VkUtils.prepareMessageText(message.text ?: "") + - (if (message.isUpdated()) updSpacer else "") + - timeSpacer + - (if (!message.isOut && message.isRead(conversation)) "" else messageStateSpacer) - - val visualizedText = - VkUtils.visualizeMentions( - preparedText, - mentionColor, - onMentionClick = { id -> - Toast.makeText(context, "id: $id", Toast.LENGTH_SHORT).show() - } - ) - - text.text = visualizedText - } - } - } - - private fun prepareAvatar( - messageUser: VkUser? = null, - messageGroup: VkGroup? = null - ) { - if (avatar != null) { - val avatarUrl = VkUtils.getMessageAvatar(message, messageUser, messageGroup) - - avatar.loadWithGlide { - imageUrl = avatarUrl - crossFade = true - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/di/MessagesHistoryModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/di/MessagesHistoryModule.kt deleted file mode 100644 index 17f1816a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/di/MessagesHistoryModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.messages.di - -import com.meloda.fast.screens.messages.MessagesHistoryViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val messagesHistoryModule = module { - viewModelOf(::MessagesHistoryViewModel) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt deleted file mode 100644 index ca0f2051..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.meloda.fast.screens.photos - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import com.meloda.fast.base.BaseFragment -import org.koin.androidx.viewmodel.ext.android.viewModel - -class PhotoViewFragment : BaseFragment() { - - private val viewModel: PhotoViewViewModel by viewModel() - -// private val photosList: MutableList = mutableListOf() - - private var photoLink: String? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - photoLink = requireArguments().getString("photoLink") - -// val list: List<*>? = Gson().fromJson( -// requireArguments().getString("photosList"), -// List::class.java -// ) -// -// list?.forEach { if (it is VkPhoto) photosList.add(it) } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ImageView(requireContext()) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - photoLink?.let { viewModel.loadImageFromUrl(it, requireView() as ImageView) } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt deleted file mode 100644 index 7eb6dc1b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.screens.photos - -import android.widget.ImageView -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import kotlinx.coroutines.launch - -class PhotoViewViewModel : ViewModel() { - - fun loadImageFromUrl( - url: String, - imageView: ImageView, - ) = viewModelScope.launch { - imageView.loadWithGlide { imageUrl = url } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/di/PhotoViewDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/di/PhotoViewDI.kt deleted file mode 100644 index 320f75ba..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/photos/di/PhotoViewDI.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.photos.di - -import com.meloda.fast.screens.photos.PhotoViewViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val photoViewModule = module { - viewModelOf(::PhotoViewViewModel) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsFragment.kt deleted file mode 100644 index 116e99c1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsFragment.kt +++ /dev/null @@ -1,338 +0,0 @@ -package com.meloda.fast.screens.settings - -import android.annotation.SuppressLint -import android.app.StatusBarManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.core.os.bundleOf -import androidx.fragment.app.setFragmentResult -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.compose.MaterialDialog -import com.meloda.fast.ext.* -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.main.MainFragment -import com.meloda.fast.screens.main.activity.LongPollUtils -import com.meloda.fast.screens.main.activity.MainActivity -import com.meloda.fast.screens.settings.items.* -import com.meloda.fast.screens.settings.model.OnSettingsChangeListener -import com.meloda.fast.screens.settings.model.OnSettingsClickListener -import com.meloda.fast.screens.settings.model.OnSettingsLongClickListener -import com.meloda.fast.screens.settings.model.SettingsItem -import com.meloda.fast.screens.testing.TestActivity -import com.meloda.fast.service.LongPollQSTileService -import com.meloda.fast.ui.AppTheme -import kotlinx.coroutines.flow.update -import org.koin.androidx.viewmodel.ext.android.viewModel - -class SettingsFragment : BaseFragment() { - - private val viewModel: SettingsViewModel by viewModel() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - listenViewModel() - - (view as? ComposeView)?.setContent { SettingsScreen() } - } - - private fun listenViewModel() { - viewModel.isLongPollBackgroundEnabled.listenValue(::handleLongPollEnabled) - viewModel.isNeedToOpenTestingActivity.listenValue(::handleOpenTestingActivity) - viewModel.isNeedToShowPerformCrashAlert.listenValue(::handlePerformCrashAlert) - viewModel.isNeedToShowAddQuickSettingsTileAlert.listenValue(::handleShowAddQuickSettingsTileAlert) - } - - private fun handleLongPollEnabled(newValue: Boolean?) { - if (newValue == null) return - - // TODO: 08.04.2023, Danil Nikolaev: rewrite this - LongPollUtils.requestNotificationsPermission( - fragmentActivity = requireActivity(), - onStateChangedAction = { newState -> MainActivity.longPollState.update { newState } }, - fromSettings = true - ) - } - - private fun handleOpenTestingActivity(newValue: Boolean) { - if (newValue) { - viewModel.onTestingActivityOpened() - context?.startActivity(Intent(context, TestActivity::class.java)) - } - } - - private fun handlePerformCrashAlert(newValue: Boolean) { - if (newValue) { - context?.showDialog( - title = UiText.Simple("Perform Crash"), - message = UiText.Simple("App will be crashed. Are you sure?"), - positiveText = UiText.Resource(R.string.yes), - positiveAction = viewModel::onPerformCrashPositiveButtonClicked, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onPerformCrashAlertDismissed - ) - } - } - - @SuppressLint("WrongConstant") - private fun handleShowAddQuickSettingsTileAlert(newValue: Boolean) { - if (newValue) { - viewModel.onAddQuickSettingsTileAlertShown() - - if (Build.VERSION.SDK_INT >= 33) { - val statusBarManager = - requireContext().getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager - statusBarManager.requestAddTileService( - ComponentName( - requireActivity(), LongPollQSTileService::class.java - ), - "Open Settings", - android.graphics.drawable.Icon.createWithResource( - requireActivity(), - R.drawable.ic_round_settings_24 - ), - {}, - {} - ) - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun SettingsScreen() { - val view = LocalView.current - - val useDynamicColors by viewModel.useDynamicColors.collectAsStateWithLifecycle() - val useLargeTopAppBar by viewModel.useLargeTopAppBar.collectAsStateWithLifecycle() - val isMultilineEnabled by viewModel.isMultilineEnabled.collectAsStateWithLifecycle() - val settings by viewModel.settings.collectAsStateWithLifecycle() - - val isNeedToShowLogOutDialog by viewModel.isNeedToShowLogOutAlert.collectAsStateWithLifecycle() - - val useHaptics by viewModel.isNeedToUseHaptics.collectAsStateWithLifecycle() - val hapticType = useHaptics.getHaptic() - view.performHapticFeedback(hapticType) - - HandleDialogs( - isNeedToShowLogOutDialog = isNeedToShowLogOutDialog - ) - - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - rememberTopAppBarState() - ) - val scaffoldModifier = if (useLargeTopAppBar) { - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - } else { - Modifier.fillMaxSize() - } - - val clickListener = OnSettingsClickListener(viewModel::onSettingsItemClicked) - val longClickListener = OnSettingsLongClickListener(viewModel::onSettingsItemLongClicked) - val changeListener = OnSettingsChangeListener(viewModel::onSettingsItemChanged) - - // TODO: 17.04.2023, Danil Nikolaev: make it work - val systemUiController = rememberSystemUiController() - DisposableEffect(systemUiController) { - systemUiController.systemBarsDarkContentEnabled = !isSystemUsingDarkMode() - onDispose {} - } - - AppTheme(useDynamicColors = useDynamicColors) { - Scaffold( - modifier = scaffoldModifier, - topBar = { - val title = @Composable { Text(text = "Settings") } - val navigationIcon = @Composable { - IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) { - Icon( - painter = painterResource(id = R.drawable.ic_round_arrow_back_24), - contentDescription = null - ) - } - } - if (useLargeTopAppBar) { - LargeTopAppBar( - title = title, - navigationIcon = navigationIcon, - scrollBehavior = scrollBehavior - ) - } else { - TopAppBar( - title = title, - navigationIcon = navigationIcon - ) - } - } - ) { padding -> - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(padding) - ) { - items( - count = settings.size, - key = { index -> - val item = settings[index] - (item.title ?: item.summary).notNull() - } - ) { index -> - when (val item = settings[index]) { - is SettingsItem.Title -> TitleSettingsItem( - item = item, - isMultiline = isMultilineEnabled - ) - - is SettingsItem.TitleSummary -> TitleSummarySettingsItem( - item = item, - isMultiline = isMultilineEnabled, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener - ) - - is SettingsItem.Switch -> SwitchSettingsItem( - item = item, - isMultiline = isMultilineEnabled, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener - ) - - is SettingsItem.TextField -> EditTextSettingsItem( - item = item, - isMultiline = isMultilineEnabled, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener - ) - - is SettingsItem.ListItem -> ListSettingsItem( - item = item, - isMultiline = isMultilineEnabled, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener - ) - } - } - } - } - } - } - - @Composable - fun HandleDialogs( - isNeedToShowLogOutDialog: Boolean - ) { - if (isNeedToShowLogOutDialog) { - val isEasterEgg = UserConfig.userId == ID_DMITRY - - val title = UiText.Resource( - if (isEasterEgg) R.string.easter_egg_log_out_dmitry - else R.string.sign_out_confirm_title - ) - - val positiveText = UiText.Resource( - if (isEasterEgg) R.string.easter_egg_log_out_dmitry - else R.string.action_sign_out - ) - - MaterialDialog( - title = title, - message = UiText.Resource(R.string.sign_out_confirm), - positiveText = positiveText, - positiveAction = { - setFragmentResult( - MainFragment.START_SERVICES_KEY, - bundleOf(MainFragment.START_SERVICES_ARG_ENABLE to false) - ) - viewModel.onLogOutAlertPositiveClick() - }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onLogOutAlertDismissed - ) - } - } - - companion object { - fun newInstance(): SettingsFragment = SettingsFragment() - - const val KEY_ACCOUNT = "account" - const val KEY_ACCOUNT_LOGOUT = "account_logout" - - const val KEY_APPEARANCE = "appearance" - const val KEY_APPEARANCE_MULTILINE = "appearance_multiline" - const val DEFAULT_VALUE_MULTILINE = true - - const val KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL = "features_hide_keyboard_on_scroll" - const val KEY_FEATURES_FAST_TEXT = "features_fast_text" - const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯" - const val KEY_FEATURES_LONG_POLL_IN_BACKGROUND = "features_lp_background" - const val DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND = true - - const val KEY_VISIBILITY_SEND_ONLINE_STATUS = "visibility_send_online_status" - - const val KEY_UPDATES_CHECK_AT_STARTUP = "updates_check_at_startup" - const val KEY_UPDATES_CHECK_UPDATES = "updates_check_updates" - - const val KEY_MS_APPCENTER_ENABLE = "msappcenter.enable" - const val KEY_MS_APPCENTER_ENABLE_ON_DEBUG = "msappcenter.enable_on_debug" - - const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash" - const val KEY_USE_DYNAMIC_COLORS = "debug_use_dynamic_colors" - const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false - const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" - const val KEY_APPEARANCE_DARK_THEME = "debug_appearance_dark_theme" - const val DEFAULT_VALUE_APPEARANCE_DARK_THEME = AppCompatDelegate.MODE_NIGHT_NO - const val KEY_USE_LARGE_TOP_APP_BAR = "debug_large_top_app_bar" - const val DEFAULT_VALUE_USE_LARGE_TOP_APP_BAR = true - const val KEY_OPEN_TESTING_ACTIVITY = "debug_open_testing_activity" - - const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list" - - const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" - - const val KEY_SHOW_EXACT_TIME_ON_TIME_STAMP = "show_exact_time_on_time_stamp" - - const val KEY_SHOW_ADD_QS_TILE_ALERT = "show_add_qs_tile_alert" - - - const val ID_DMITRY = 37610580 - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsViewModel.kt deleted file mode 100644 index 4f6bb202..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsViewModel.kt +++ /dev/null @@ -1,526 +0,0 @@ -package com.meloda.fast.screens.settings - -import android.os.Build -import android.view.HapticFeedbackConstants -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.edit -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.github.terrakok.cicerone.Router -import com.meloda.fast.BuildConfig -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.Screens -import com.meloda.fast.data.account.AccountsDao -import com.meloda.fast.database.CacheDatabase -import com.meloda.fast.ext.emitOnMainScope -import com.meloda.fast.ext.ifEmpty -import com.meloda.fast.ext.isSdkAtLeast -import com.meloda.fast.ext.isTrue -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString -import com.meloda.fast.screens.main.activity.LongPollState -import com.meloda.fast.screens.main.activity.MainActivity -import com.meloda.fast.screens.settings.model.SettingsItem -import com.microsoft.appcenter.crashes.model.TestCrashException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -typealias SettingsList = List> - -interface SettingsViewModel { - - val settings: StateFlow - val useDynamicColors: StateFlow - val useLargeTopAppBar: StateFlow - val isMultilineEnabled: StateFlow - val isLongPollBackgroundEnabled: StateFlow - - val isNeedToShowLogOutAlert: StateFlow - - val isNeedToOpenTestingActivity: StateFlow - - val isNeedToShowPerformCrashAlert: StateFlow - - val isNeedToShowAddQuickSettingsTileAlert: StateFlow - - val isNeedToUseHaptics: StateFlow - - fun onLogOutAlertDismissed() - - fun onPerformCrashAlertDismissed() - - fun onPerformCrashPositiveButtonClicked() - - fun onLogOutAlertPositiveClick() - - fun onSettingsItemClicked(key: String) - fun onSettingsItemLongClicked(key: String): Boolean - fun onSettingsItemChanged(key: String, newValue: Any?) - - fun onTestingActivityOpened() - - fun onAddQuickSettingsTileAlertShown() - - fun onHapticsUsed() -} - -class SettingsViewModelImpl constructor( - private val accountsDao: AccountsDao, - private val cacheDatabase: CacheDatabase, - private val router: Router -) : SettingsViewModel, ViewModel() { - - override val settings = MutableStateFlow(emptyList()) - override val useDynamicColors = MutableStateFlow( - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_DYNAMIC_COLORS, - SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - ) - override val useLargeTopAppBar = MutableStateFlow( - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_LARGE_TOP_APP_BAR, - SettingsFragment.DEFAULT_VALUE_USE_LARGE_TOP_APP_BAR - ) - ) - override val isMultilineEnabled = MutableStateFlow( - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_APPEARANCE_MULTILINE, - SettingsFragment.DEFAULT_VALUE_MULTILINE - ) - ) - override val isLongPollBackgroundEnabled = MutableStateFlow(null) - - override val isNeedToShowLogOutAlert = MutableStateFlow(false) - - override val isNeedToOpenTestingActivity = MutableStateFlow(false) - - override val isNeedToShowPerformCrashAlert = MutableStateFlow(false) - - override val isNeedToShowAddQuickSettingsTileAlert = MutableStateFlow(false) - - override val isNeedToUseHaptics = MutableStateFlow(HapticType.None) - - init { - if (AppGlobal.preferences.getBoolean("first_open_settings", true)) { - AppGlobal.preferences.edit { - putBoolean("first_open_settings", false) - } - - isNeedToShowAddQuickSettingsTileAlert.emitOnMainScope(true) - } - - createSettings() - } - - private fun createSettings() { - viewModelScope.launch { - val accountVisible = UserConfig.isLoggedIn() - val accountTitle = SettingsItem.Title.build( - key = SettingsFragment.KEY_ACCOUNT, - title = UiText.Simple("Account") - ) { - isVisible = accountVisible - } - val accountLogOut = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_ACCOUNT_LOGOUT, - title = UiText.Simple("Log out"), - summary = UiText.Simple("Log out from account and delete all local data related to this account") - ) { - isVisible = accountVisible - } - - val appearanceTitle = SettingsItem.Title.build( - key = SettingsFragment.KEY_APPEARANCE, - title = UiText.Simple("Appearance") - ) - val appearanceMultiline = SettingsItem.Switch.build( - key = SettingsFragment.KEY_APPEARANCE_MULTILINE, - defaultValue = SettingsFragment.DEFAULT_VALUE_MULTILINE, - title = UiText.Simple("Multiline titles and messages"), - summary = UiText.Simple("The title of the dialog and the text of the message can take up two lines") - ) - - val featuresTitle = SettingsItem.Title.build( - key = "features", - title = UiText.Simple("Features") - ) - val featuresHideKeyboardOnScroll = SettingsItem.Switch.build( - key = SettingsFragment.KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL, - defaultValue = true, - title = UiText.Simple("Hide keyboard on scroll"), - summary = UiText.Simple("Hides keyboard when you scrolling messages up in messages history screen") - ) - val featuresFastText = SettingsItem.TextField.build( - key = SettingsFragment.KEY_FEATURES_FAST_TEXT, - title = UiText.Simple("Fast text"), - defaultValue = "¯\\_(ツ)_/¯", - ).apply { - summaryProvider = SettingsItem.SummaryProvider { settingsItem -> - UiText.ResourceParams( - R.string.pref_message_fast_text_summary, - listOf(settingsItem.value.ifEmpty { null }) - ) - } - } - val featuresLongPollBackground = SettingsItem.Switch.build( - key = SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - defaultValue = SettingsFragment.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND, - title = UiText.Simple("LongPoll in background"), - summary = UiText.Simple("Your messages will be updates even when app is not on the screen") - ) - - val visibilityTitle = SettingsItem.Title.build( - key = "visibility", - title = UiText.Simple("Visibility") - ) - val visibilitySendOnlineStatus = SettingsItem.Switch.build( - key = SettingsFragment.KEY_VISIBILITY_SEND_ONLINE_STATUS, - defaultValue = false, - title = UiText.Simple("Send online status"), - summary = UiText.Simple("Online status will be sent every five minutes") - ) - - val updatesTitle = SettingsItem.Title.build( - key = "updates", - title = UiText.Simple("Updates") - ) - val updatesCheckAtStartup = SettingsItem.Switch.build( - key = SettingsFragment.KEY_UPDATES_CHECK_AT_STARTUP, - title = UiText.Simple("Check at startup"), - summary = UiText.Simple("Check updates at app startup"), - defaultValue = true - ) - val updatesCheckUpdates = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_UPDATES_CHECK_UPDATES, - title = UiText.Simple("Check updates") - ) - - val msAppCenterTitle = SettingsItem.Title.build( - key = "msappcenter", - title = UiText.Simple("MS AppCenter Crash Reporter") - ) - val msAppCenterEnable = SettingsItem.Switch.build( - key = SettingsFragment.KEY_MS_APPCENTER_ENABLE, - defaultValue = true, - title = UiText.Simple("Enable Crash Reporter") - ) - val msAppCenterEnableOnDebug = SettingsItem.Switch.build( - key = SettingsFragment.KEY_MS_APPCENTER_ENABLE_ON_DEBUG, - defaultValue = false, - title = UiText.Simple("Enable Crash Reporter on debug builds"), - summary = UiText.Simple("Requires application restart") - ) - - val debugTitle = SettingsItem.Title.build( - key = "debug", - title = UiText.Simple("Debug") - ) - val debugPerformCrash = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_DEBUG_PERFORM_CRASH, - title = UiText.Simple("Perform crash"), - summary = UiText.Simple("App will be crashed. Obviously") - ) - val debugShowCrashAlert = SettingsItem.Switch.build( - key = SettingsFragment.KEY_DEBUG_SHOW_CRASH_ALERT, - defaultValue = true, - title = UiText.Simple("Show alert after crash"), - summary = UiText.Simple("Shows alert dialog with stacktrace after app crashed\n(it will be not shown if you perform crash manually)") - ) - val debugUseDynamicColors = SettingsItem.Switch.build( - key = SettingsFragment.KEY_USE_DYNAMIC_COLORS, - title = UiText.Simple("[WIP] Use dynamic colors"), - isEnabled = isSdkAtLeast(Build.VERSION_CODES.S), - summary = UiText.Simple("Requires Android 12 or higher;\nUnstable - you may need to manually kill app via it's info screen in order for changes to applied"), - defaultValue = false - ) - - val darkThemeValues = listOf( - AppCompatDelegate.MODE_NIGHT_YES, - AppCompatDelegate.MODE_NIGHT_NO, - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, - AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY - ) - val darkThemeTitles = listOf( - UiText.Simple("Enabled"), - UiText.Simple("Disabled"), - UiText.Simple("Follow system"), - UiText.Simple("Battery saver") - ) - val darkThemeValuesMap = List(darkThemeValues.size) { index -> - darkThemeValues[index] to darkThemeTitles[index].parseString(AppGlobal.Instance) - }.toMap() - - val debugDarkTheme = SettingsItem.ListItem.build( - key = SettingsFragment.KEY_APPEARANCE_DARK_THEME, - title = UiText.Simple("[WIP] Dark theme"), - values = darkThemeValues, - valueTitles = darkThemeTitles, - defaultValue = AppCompatDelegate.MODE_NIGHT_NO - ) { - summaryProvider = SettingsItem.SummaryProvider { item -> - UiText.Simple( - "Current value: ${ - darkThemeValuesMap.getOrElse(item.value ?: -1) { - "Unknown" - } - }" - ) - } - } - val debugUseLargeTopAppBar = SettingsItem.Switch.build( - key = SettingsFragment.KEY_USE_LARGE_TOP_APP_BAR, - title = UiText.Simple("[WIP] Use LargeTopAppBar"), - summary = UiText.Simple("Using large top appbar instead of default toolbar everywhere in app"), - defaultValue = SettingsFragment.DEFAULT_VALUE_USE_LARGE_TOP_APP_BAR - ) - val debugOpenTestingActivity = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_OPEN_TESTING_ACTIVITY, - title = UiText.Simple("Open testing activity") - ) - val debugShowExactTimeOnTimeStamp = SettingsItem.Switch.build( - key = SettingsFragment.KEY_SHOW_EXACT_TIME_ON_TIME_STAMP, - title = UiText.Simple("Show exact time on time stamp"), - summary = UiText.Simple("Shows hours and minutes on time stamp in messages history"), - defaultValue = false - ) - val debugShowAddQuickSettingsTileAlert = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_SHOW_ADD_QS_TILE_ALERT, - title = UiText.Simple("Add QuickSettings Tile") - ) - - val debugHideDebugList = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_DEBUG_HIDE_DEBUG_LIST, - title = UiText.Simple("Hide debug list") - ) - - val accountList = listOf( - accountTitle, - accountLogOut - ) - val appearanceList = listOf( - appearanceTitle, - appearanceMultiline - ) - val featuresList = listOf( - featuresTitle, - featuresHideKeyboardOnScroll, - featuresFastText, - featuresLongPollBackground - ) - val visibilityList = listOf( - visibilityTitle, - visibilitySendOnlineStatus, - ) - val updatesList = listOf( - updatesTitle, - updatesCheckAtStartup, - updatesCheckUpdates, - ) - val msAppCenterList = mutableListOf( - msAppCenterTitle, - msAppCenterEnable, - ).apply { - if (BuildConfig.DEBUG) { - this += msAppCenterEnableOnDebug - } - } - val debugList = mutableListOf>() - listOf( - debugTitle, - debugPerformCrash, - debugShowCrashAlert, - debugUseDynamicColors, - debugDarkTheme, - debugUseLargeTopAppBar, - debugOpenTestingActivity, - debugShowExactTimeOnTimeStamp, - debugShowAddQuickSettingsTileAlert - ).forEach(debugList::add) - - debugList += debugHideDebugList - - val settingsList = mutableListOf>() - listOf( - accountList, - appearanceList, - featuresList, - visibilityList, - updatesList, - msAppCenterList, - debugList, - ).forEach(settingsList::addAll) - - if (!AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, - false - ) - ) { - settingsList.removeAll(debugList) - } - - settings.emit(settingsList) - } - } - - override fun onLogOutAlertDismissed() { - viewModelScope.launch(Dispatchers.Main) { - isNeedToShowLogOutAlert.emit(false) - } - } - - override fun onPerformCrashAlertDismissed() { - isNeedToShowPerformCrashAlert.emitOnMainScope(false) - } - - override fun onPerformCrashPositiveButtonClicked() { - isNeedToShowPerformCrashAlert.emitOnMainScope(false) - throw TestCrashException() - } - - override fun onLogOutAlertPositiveClick() { - viewModelScope.launch(Dispatchers.IO) { - accountsDao.deleteById(UserConfig.userId) - cacheDatabase.clearAllTables() - - MainActivity.longPollState.emit(LongPollState.Stop) - - UserConfig.clear() - - withContext(Dispatchers.Main) { - router.newRootScreen(Screens.Main()) - } - } - } - - override fun onSettingsItemClicked(key: String) { - when (key) { - SettingsFragment.KEY_ACCOUNT_LOGOUT -> { - viewModelScope.launch(Dispatchers.Main) { - isNeedToShowLogOutAlert.emit(true) - } - } - - SettingsFragment.KEY_UPDATES_CHECK_UPDATES -> { - openUpdatesScreen() - } - - SettingsFragment.KEY_DEBUG_PERFORM_CRASH -> { - isNeedToShowPerformCrashAlert.emitOnMainScope(true) - } - - SettingsFragment.KEY_DEBUG_HIDE_DEBUG_LIST -> { - val showDebugCategory = - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, - false - ) - if (!showDebugCategory) return - - AppGlobal.preferences.edit { - putBoolean(SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, false) - } - - createSettings() - - isNeedToUseHaptics.emitOnMainScope(HapticType.HideDebugMenu) - } - - SettingsFragment.KEY_OPEN_TESTING_ACTIVITY -> { - isNeedToOpenTestingActivity.emitOnMainScope(true) - } - - SettingsFragment.KEY_SHOW_ADD_QS_TILE_ALERT -> { - isNeedToShowAddQuickSettingsTileAlert.emitOnMainScope(true) - } - } - } - - override fun onSettingsItemLongClicked(key: String): Boolean { - return when (key) { - SettingsFragment.KEY_UPDATES_CHECK_UPDATES -> { - val showDebugCategory = - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, - false - ) - if (showDebugCategory) return false - - AppGlobal.preferences.edit { - putBoolean(SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, true) - } - createSettings() - - isNeedToUseHaptics.emitOnMainScope(HapticType.ShowDebugMenu) - true - } - - else -> false - } - } - - override fun onSettingsItemChanged(key: String, newValue: Any?) { - when (key) { - SettingsFragment.KEY_APPEARANCE_DARK_THEME -> { - val newMode = newValue as? Int ?: return - AppCompatDelegate.setDefaultNightMode(newMode) - } - - SettingsFragment.KEY_APPEARANCE_MULTILINE -> { - val isEnabled = (newValue as? Boolean).isTrue - isMultilineEnabled.update { isEnabled } - } - - SettingsFragment.KEY_USE_DYNAMIC_COLORS -> { - val isEnabled = (newValue as? Boolean).isTrue - useDynamicColors.update { isEnabled } - } - - SettingsFragment.KEY_USE_LARGE_TOP_APP_BAR -> { - val isEnabled = (newValue as? Boolean).isTrue - useLargeTopAppBar.update { isEnabled } - } - - SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> { - val isEnabled = (newValue as? Boolean).isTrue - isLongPollBackgroundEnabled.update { isEnabled } - } - } - } - - override fun onTestingActivityOpened() { - isNeedToOpenTestingActivity.emitOnMainScope(false) - } - - override fun onAddQuickSettingsTileAlertShown() { - isNeedToShowAddQuickSettingsTileAlert.emitOnMainScope(false) - } - - override fun onHapticsUsed() { - isNeedToUseHaptics.emitOnMainScope(HapticType.None) - } - - private fun openUpdatesScreen() { - router.navigateTo(Screens.Updates()) - } -} - -sealed interface HapticType { - object ShowDebugMenu : HapticType - object HideDebugMenu : HapticType - object None : HapticType - - fun getHaptic(): Int { - return when (this) { - ShowDebugMenu -> HapticFeedbackConstants.LONG_PRESS - HideDebugMenu -> HapticFeedbackConstants.REJECT - None -> -1 - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/di/SettingsModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/di/SettingsModule.kt deleted file mode 100644 index f9dd86c6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/di/SettingsModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.settings.di - -import com.meloda.fast.screens.settings.SettingsViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val settingsModule = module { - viewModelOf(::SettingsViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/ListSettingsItem.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/items/ListSettingsItem.kt deleted file mode 100644 index c742e7c7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/ListSettingsItem.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.meloda.fast.screens.settings.items - -import android.content.Context -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.fast.R -import com.meloda.fast.ext.ItemsChoiceType -import com.meloda.fast.ext.getString -import com.meloda.fast.ext.showDialog -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.settings.model.OnSettingsChangeListener -import com.meloda.fast.screens.settings.model.OnSettingsClickListener -import com.meloda.fast.screens.settings.model.OnSettingsLongClickListener -import com.meloda.fast.screens.settings.model.SettingsItem - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ListSettingsItem( - item: SettingsItem.ListItem, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener, - onSettingsChangeListener: OnSettingsChangeListener -) { - val context = LocalContext.current - - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } - - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - if (!isVisible) return - Row( - modifier = Modifier - .heightIn(min = 56.dp) - .fillMaxWidth() - .combinedClickable( - enabled = isEnabled, - onClick = { - onSettingsClickListener.onClick(item.key) - showListAlertDialog( - context = context, - item = item, - onSettingsChangeListener = { key, newValue -> - summary = item.summaryProvider?.provideSummary(item) - onSettingsChangeListener.onChange(key, newValue) - } - ) - }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, - ) - ) { - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - Spacer(modifier = Modifier.height(14.dp)) - title?.getString()?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - summary?.getString()?.let { summary -> - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - Spacer(modifier = Modifier.height(14.dp)) - } - Spacer(modifier = Modifier.width(16.dp)) - } -} - -private fun showListAlertDialog( - context: Context, - item: SettingsItem.ListItem, - onSettingsChangeListener: OnSettingsChangeListener -) { - var selectedOption = item.value - val checkedItem = item.values.indexOf(selectedOption) - - context.showDialog( - title = item.title, - items = item.valueTitles, - checkedItems = listOf(checkedItem), - itemsChoiceType = ItemsChoiceType.SingleChoice, - itemsClickAction = { index, _ -> - selectedOption = item.values[index] - }, - positiveText = UiText.Resource(R.string.ok), - positiveAction = { - if (item.value != selectedOption) { - item.value = selectedOption - onSettingsChangeListener.onChange(item.key, selectedOption) - } - } - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TextFieldSettingsItem.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TextFieldSettingsItem.kt deleted file mode 100644 index 1639e0e9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TextFieldSettingsItem.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.meloda.fast.screens.settings.items - -import android.content.Context -import android.view.LayoutInflater -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.fast.R -import com.meloda.fast.compose.MaterialDialog -import com.meloda.fast.databinding.ItemSettingsEditTextAlertBinding -import com.meloda.fast.ext.getString -import com.meloda.fast.ext.showDialog -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.settings.model.OnSettingsChangeListener -import com.meloda.fast.screens.settings.model.OnSettingsClickListener -import com.meloda.fast.screens.settings.model.OnSettingsLongClickListener -import com.meloda.fast.screens.settings.model.SettingsItem - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun EditTextSettingsItem( - item: SettingsItem.TextField, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener, - onSettingsChangeListener: OnSettingsChangeListener -) { - val context = LocalContext.current - - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } - - // TODO: 07.04.2023, Danil Nikolaev: handle isEnabled - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - var showDialog by remember { - mutableStateOf(false) - } - - if (showDialog) { - ShowEditTextAlert( - item = item, - onSettingsChangeListener = { key, newValue -> - summary = item.summaryProvider?.provideSummary(item) - onSettingsChangeListener.onChange(key, newValue) - }, - onDismiss = { showDialog = false } - ) - } - - if (!isVisible) return - Row( - modifier = Modifier - .heightIn(min = 56.dp) - .fillMaxWidth() - .combinedClickable( - enabled = isEnabled, - onClick = { - onSettingsClickListener.onClick(item.key) - showDialog = true - }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, - ) - ) { - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - Spacer(modifier = Modifier.height(14.dp)) - title?.getString()?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - summary?.getString()?.let { summary -> - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - Spacer(modifier = Modifier.height(14.dp)) - } - Spacer(modifier = Modifier.width(16.dp)) - } -} - -private fun showEditTextAlert( - context: Context, - item: SettingsItem.TextField, - onSettingsChangeListener: OnSettingsChangeListener -) { - val binding = ItemSettingsEditTextAlertBinding.inflate( - LayoutInflater.from(context), null, false - ) - - binding.editText.setText(item.value) - - context.showDialog( - title = item.title, - view = binding.root, - positiveText = UiText.Resource(R.string.ok), - positiveAction = { - val newValue = binding.editText.text.toString() - - if (item.value != newValue) { - item.value = newValue - onSettingsChangeListener.onChange(item.key, newValue) - } - }, - negativeText = UiText.Resource(R.string.cancel) - ) -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) -@Composable -fun ShowEditTextAlert( - item: SettingsItem.TextField, - onSettingsChangeListener: OnSettingsChangeListener, - onDismiss: () -> Unit -) { - val (textFieldFocusable) = FocusRequester.createRefs() - - var textFieldValue by remember { - mutableStateOf(TextFieldValue(item.value.orEmpty())) - } - - MaterialDialog( - title = item.title, - positiveText = UiText.Resource(R.string.ok), - positiveAction = { - val newValue = textFieldValue.text.trim() - - if (item.value != newValue) { - item.value = newValue - onSettingsChangeListener.onChange(item.key, newValue) - } - }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = onDismiss - ) { - TextField( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .focusRequester(textFieldFocusable), - value = textFieldValue, - onValueChange = { newText -> - textFieldValue = newText - }, - label = { Text(text = "Value") }, - placeholder = { Text(text = "Value") }, - shape = RoundedCornerShape(10.dp), - ) - } - - LaunchedEffect(Unit) { - textFieldFocusable.requestFocus() - textFieldValue = textFieldValue.copy(selection = TextRange(textFieldValue.text.length)) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TitleSummarySettingsItem.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TitleSummarySettingsItem.kt deleted file mode 100644 index 89642d38..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TitleSummarySettingsItem.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.meloda.fast.screens.settings.items - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.fast.ext.getString -import com.meloda.fast.screens.settings.model.OnSettingsClickListener -import com.meloda.fast.screens.settings.model.OnSettingsLongClickListener -import com.meloda.fast.screens.settings.model.SettingsItem - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun TitleSummarySettingsItem( - item: SettingsItem.TitleSummary, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener -) { - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } - - // TODO: 08.04.2023, Danil Nikolaev: handle isEnabled state - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - if (!isVisible) return - Row( - modifier = Modifier - .heightIn(min = 56.dp) - .fillMaxWidth() - .combinedClickable( - enabled = isEnabled, - onClick = { onSettingsClickListener.onClick(item.key) }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, - ) - ) { - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - Spacer(modifier = Modifier.height(14.dp)) - title?.getString()?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - summary?.getString()?.let { summary -> - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - Spacer(modifier = Modifier.height(14.dp)) - } - Spacer(modifier = Modifier.width(16.dp)) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/testing/TestActivity.kt b/app/src/main/kotlin/com/meloda/fast/screens/testing/TestActivity.kt deleted file mode 100644 index 147fabd2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/testing/TestActivity.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.meloda.fast.screens.testing - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.edgeToEdge -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.ui.AppTheme - -class TestActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - edgeToEdge() - - setContent { - TestingScreen() - } - } - - @Preview - @Composable - fun TestingScreenPreview() { - TestingScreen() - } - - @Composable - fun TestingScreen() { - val useDynamicColors = AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_DYNAMIC_COLORS, - SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - AppTheme(useDynamicColors = useDynamicColors) { - Scaffold(modifier = Modifier.fillMaxSize()) { padding -> - Surface( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Column { - Button(onClick = {}) { - Text(text = "Button") - } - Text(text = "Testing text") - } - } - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/TwoFaScreens.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/TwoFaScreens.kt deleted file mode 100644 index 242c1b77..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/TwoFaScreens.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.screens.twofa - -import com.github.terrakok.cicerone.androidx.FragmentScreen -import com.meloda.fast.screens.twofa.presentation.TwoFaFragment - -object TwoFaScreens { - - fun twoFaScreen() = FragmentScreen(key = "TwoFaScreen") { - TwoFaFragment.newInstance() - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/di/TwoFaDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/di/TwoFaDI.kt deleted file mode 100644 index 62eec829..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/di/TwoFaDI.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.meloda.fast.screens.twofa.di - -import com.meloda.fast.di.navigationModule -import com.meloda.fast.screens.twofa.presentation.TwoFaViewModelImpl -import com.meloda.fast.screens.twofa.screen.TwoFaCoordinator -import com.meloda.fast.screens.twofa.screen.TwoFaCoordinatorImpl -import com.meloda.fast.screens.twofa.screen.TwoFaScreen -import com.meloda.fast.screens.twofa.validation.TwoFaValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.core.module.dsl.singleOf -import org.koin.core.qualifier.named -import org.koin.core.scope.Scope -import org.koin.dsl.bind -import org.koin.dsl.module - -val twoFaModule = module { - val moduleQualifier = named("twoFa") - - includes(navigationModule) - - single(moduleQualifier) { screen().resultFlow } - single { screen().getArguments() } - - single { - TwoFaCoordinatorImpl( - resultFlow = get(moduleQualifier), - router = get() - ) - } bind TwoFaCoordinator::class - - singleOf(::TwoFaValidator) - viewModelOf(::TwoFaViewModelImpl) -} - -private fun Scope.screen(): TwoFaScreen = get() diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaArguments.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaArguments.kt deleted file mode 100644 index f0b71e42..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaArguments.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.screens.twofa.model - -import com.meloda.fast.model.base.UiText - -data class TwoFaArguments( - val validationSid: String, - val redirectUri: String, - val phoneMask: String, - val validationType: TwoFaValidationType, - val canResendSms: Boolean, - val wrongCodeError: UiText?, -) diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaResult.kt deleted file mode 100644 index 2e7d7c3e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.screens.twofa.model - -sealed class TwoFaResult { - object Cancelled : TwoFaResult() - data class Success(val sid: String, val code: String) : TwoFaResult() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaScreenState.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaScreenState.kt deleted file mode 100644 index fe9c518c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaScreenState.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.meloda.fast.screens.twofa.model - -import com.meloda.fast.model.base.UiText - -data class TwoFaScreenState( - val twoFaSid: String, - val twoFaCode: String, - val twoFaText: UiText, - val canResendSms: Boolean, - val codeError: UiText?, - val delayTime: Int -) { - - companion object { - val EMPTY = TwoFaScreenState( - twoFaSid = "", - twoFaCode = "", - twoFaText = UiText.Simple(""), - canResendSms = false, - codeError = null, - delayTime = 0 - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaValidationResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaValidationResult.kt deleted file mode 100644 index 188dbc99..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaValidationResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.screens.twofa.model - -sealed class TwoFaValidationResult { - object Empty : TwoFaValidationResult() - object Valid : TwoFaValidationResult() - - fun isValid() = this == Valid -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/presentation/TwoFaViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/presentation/TwoFaViewModel.kt deleted file mode 100644 index eddb0ca5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/presentation/TwoFaViewModel.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.meloda.fast.screens.twofa.presentation - -import androidx.lifecycle.viewModelScope -import com.meloda.fast.base.viewmodel.BaseViewModel -import com.meloda.fast.data.auth.AuthRepository -import com.meloda.fast.ext.createTimerFlow -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.updateValue -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.twofa.model.TwoFaArguments -import com.meloda.fast.screens.twofa.model.TwoFaResult -import com.meloda.fast.screens.twofa.model.TwoFaScreenState -import com.meloda.fast.screens.twofa.model.TwoFaValidationType -import com.meloda.fast.screens.twofa.screen.TwoFaCoordinator -import com.meloda.fast.screens.twofa.validation.TwoFaValidator -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.launch - - -interface TwoFaViewModel { - - val screenState: StateFlow - - fun onCodeInputChanged(newCode: String) - - fun onBackButtonClicked() - fun onCancelButtonClicked() - fun onRequestSmsButtonClicked() - fun onTextFieldDoneClicked() - fun onDoneButtonClicked() -} - -class TwoFaViewModelImpl constructor( - private val coordinator: TwoFaCoordinator, - private val validator: TwoFaValidator, - private val authRepository: AuthRepository, - arguments: TwoFaArguments, -) : TwoFaViewModel, BaseViewModel() { - - override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY) - - private var delayJob: Job? = null - - init { - if (arguments.wrongCodeError != null) { - screenState.updateValue( - screenState.value.copy(codeError = arguments.wrongCodeError) - ) - } - - screenState.updateValue( - screenState.value.copy( - twoFaSid = arguments.validationSid, - twoFaText = getTwoFaText(arguments.validationType), - canResendSms = arguments.canResendSms - ) - ) - } - - override fun onCodeInputChanged(newCode: String) { - screenState.updateValue( - screenState.value.copy( - twoFaCode = newCode.trim(), - codeError = null - ) - ) - - if (newCode.length == 6) { - onDoneButtonClicked() - } - } - - override fun onBackButtonClicked() { - onCancelButtonClicked() - } - - override fun onCancelButtonClicked() { - coordinator.finishWithResult(TwoFaResult.Cancelled) - } - - override fun onRequestSmsButtonClicked() { - sendValidationCode() - } - - override fun onTextFieldDoneClicked() { - onDoneButtonClicked() - } - - override fun onDoneButtonClicked() { - if (!processValidation()) return - - val twoFaSid = screenState.value.twoFaSid - val twoFaCode = screenState.value.twoFaCode - - coordinator.finishWithResult(TwoFaResult.Success(sid = twoFaSid, code = twoFaCode)) - } - - private fun processValidation(): Boolean { - val isValid = validator.validate(screenState.value).isValid() - - screenState.updateValue( - screenState.value.copy( - codeError = if (isValid) null - else UiText.Simple("Field must not be empty") - ) - ) - - return isValid - } - - private fun sendValidationCode() { - val validationSid = screenState.value.twoFaSid - - viewModelScope.launch { - sendRequest { - authRepository.sendSms(validationSid) - }?.let { response -> - val newValidationType = response.validationType - val newCanResendSms = response.validationResend == "sms" - - screenState.updateValue( - screenState.value.copy( - canResendSms = newCanResendSms, - twoFaText = getTwoFaText( - TwoFaValidationType.parse( - newValidationType ?: "null" - ) - ) - ) - ) - - startTickTimer(response.delay) - } - } - } - - private fun startTickTimer(delay: Int?) { - if (delay == null || delayJob?.isActive.isTrue) return - - delayJob = createTimerFlow( - time = delay, - onStartAction = { - screenState.updateValue( - screenState.value.copy(canResendSms = false) - ) - }, - onTickAction = { remainedTime -> - screenState.updateValue( - screenState.value.copy(delayTime = remainedTime) - ) - }, - onTimeoutAction = { - screenState.updateValue( - screenState.value.copy( - canResendSms = true - ) - ) - }, - ).launchIn(viewModelScope) - } - - private fun getTwoFaText(validationType: TwoFaValidationType): UiText { - return when (validationType) { - TwoFaValidationType.Sms -> UiText.Simple("sms") - TwoFaValidationType.TwoFaApp -> UiText.Simple("2fa app") - is TwoFaValidationType.Another -> UiText.Simple(validationType.type) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaCoordinator.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaCoordinator.kt deleted file mode 100644 index 4cbd4e9f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaCoordinator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.screens.twofa.screen - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.screens.twofa.model.TwoFaResult -import kotlinx.coroutines.flow.MutableSharedFlow - -interface TwoFaCoordinator { - - fun finishWithResult(result: TwoFaResult) -} - -class TwoFaCoordinatorImpl( - private val resultFlow: MutableSharedFlow, - private val router: Router -) : TwoFaCoordinator { - - override fun finishWithResult(result: TwoFaResult) { - resultFlow.tryEmit(result) - router.exit() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaScreen.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaScreen.kt deleted file mode 100644 index 8437f6ff..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaScreen.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.screens.twofa.screen - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.base.screen.AppScreen -import com.meloda.fast.base.screen.createResultFlow -import com.meloda.fast.screens.twofa.TwoFaScreens -import com.meloda.fast.screens.twofa.model.TwoFaArguments -import com.meloda.fast.screens.twofa.model.TwoFaResult -import kotlin.properties.Delegates - -class TwoFaScreen : AppScreen { - - override val resultFlow = createResultFlow() - - override var args: TwoFaArguments by Delegates.notNull() - - override fun show(router: Router, args: TwoFaArguments) { - this.args = args - router.navigateTo(TwoFaScreens.twoFaScreen()) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/validation/TwoFaValidator.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/validation/TwoFaValidator.kt deleted file mode 100644 index d3b54e28..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/validation/TwoFaValidator.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.screens.twofa.validation - -import com.meloda.fast.screens.twofa.model.TwoFaScreenState -import com.meloda.fast.screens.twofa.model.TwoFaValidationResult - -class TwoFaValidator { - - fun validate(screenState: TwoFaScreenState): TwoFaValidationResult { - return when { - screenState.twoFaCode.isEmpty() -> TwoFaValidationResult.Empty - else -> TwoFaValidationResult.Valid - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesFragment.kt deleted file mode 100644 index bacb2186..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesFragment.kt +++ /dev/null @@ -1,391 +0,0 @@ -package com.meloda.fast.screens.updates - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf -import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.ext.getParcelableCompat -import com.meloda.fast.ext.listenValue -import com.meloda.fast.ext.showDialog -import com.meloda.fast.ext.string -import com.meloda.fast.model.UpdateItem -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.updates.model.UpdateState -import com.meloda.fast.ui.AppTheme -import com.meloda.fast.util.AndroidUtils -import okhttp3.ResponseBody -import org.koin.androidx.viewmodel.ext.android.viewModel - - -class UpdatesFragment : BaseFragment(R.layout.fragment_updates) { - - private val viewModel: UpdatesViewModel by viewModel() - - private val changelogPlaceholder by lazy { - string(R.string.fragment_updates_changelog_none) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - UpdatesScreen() - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun UpdatesScreen() { - AppTheme { - val state by viewModel.screenState.collectAsState() - val updateState = state.updateState - val downloadProgress by viewModel.currentDownloadProgress.collectAsState() - val animatedProgress by animateFloatAsState( - targetValue = downloadProgress / 100f, - animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec - ) - - Scaffold(topBar = { Toolbar() }) { paddingValues -> - Surface( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize() - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize(), - ) { - when { - updateState.isLoading() -> CircularProgressIndicator() - updateState.isDownloading() -> { - Text( - text = getString(R.string.fragment_updates_downloading_update), - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(12.dp)) - if (animatedProgress > 0) { - LinearProgressIndicator(progress = animatedProgress) - } else { - LinearProgressIndicator() - } - Spacer(modifier = Modifier.height(12.dp)) - FilledTonalButton(onClick = viewModel::onCancelDownloadButtonClicked) { - Text(text = getString(R.string.action_stop)) - } - } - - else -> { - getTitle(updateState)?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall - ) - Spacer(modifier = Modifier.height(8.dp)) - } - - getSubtitle(updateState)?.let { subtitle -> - Text( - text = subtitle, - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(6.dp)) - } - - state.updateItem?.changelog?.let { - Text( - text = getString(R.string.fragment_updates_changelog), - style = TextStyle(textDecoration = TextDecoration.Underline), - modifier = Modifier.clickable(onClick = viewModel::onChangelogButtonClicked) - ) - } - - getActionButtonText(updateState)?.let { buttonText -> - Spacer(modifier = Modifier.height(24.dp)) - ExtendedFloatingActionButton( - onClick = viewModel::onActionButtonClicked, - modifier = Modifier, - text = { Text(text = buttonText) }, - icon = { - getActionButtonIcon(state = updateState)?.let { painter -> - Spacer(modifier = Modifier.width(4.dp)) - Icon(painter = painter, contentDescription = null) - } - } - ) - } - - if (updateState.isDownloaded()) { - Spacer(modifier = Modifier.height(48.dp)) - Text( - text = getString(R.string.fragment_updates_issues_installing), - style = TextStyle(textDecoration = TextDecoration.Underline), - modifier = Modifier.clickable(onClick = viewModel::onIssuesButtonClicked), - ) - } - } - } - } - } - } - } - } - - private fun getTitle(state: UpdateState): String? { - return when (state) { - UpdateState.Error -> R.string.error_occurred - UpdateState.NewUpdate -> R.string.fragment_updates_new_version - UpdateState.NoUpdates -> R.string.fragment_updates_no_updates - UpdateState.Downloaded -> R.string.fragment_updates_downloaded - else -> null - }?.let(requireContext()::getString) - } - - private fun getSubtitle(state: UpdateState): String? { - return when (state) { - UpdateState.Error -> { - viewModel.screenState.value.error?.let { error -> - if (error.contains("cannot be converted", ignoreCase = true) - || error.contains("begin_object", ignoreCase = true) - ) { - "OTA Server is unavailable" - } else { - string(R.string.error_occurred_description, error) - } - } - } - - UpdateState.NewUpdate, UpdateState.Downloaded -> { - viewModel.screenState.value.updateItem?.let { item -> - string( - R.string.fragment_updates_new_version_description, item.versionName - ) - } - } - - UpdateState.NoUpdates -> string(R.string.fragment_updates_no_updates_description) - else -> null - } - } - - private fun getActionButtonText(state: UpdateState): String? { - return when (state) { - UpdateState.Error -> R.string.fragment_updates_try_again - UpdateState.NewUpdate -> R.string.fragment_updates_download_update - UpdateState.NoUpdates -> R.string.fragment_updates_check_updates - UpdateState.Downloaded -> R.string.fragment_updates_install - else -> null - }?.let(requireContext()::getString) - } - - @Composable - private fun getActionButtonIcon(state: UpdateState): Painter? { - return when (state) { - UpdateState.Error -> R.drawable.round_restart_alt_24 - UpdateState.NewUpdate -> R.drawable.round_file_download_24 - UpdateState.Downloaded -> R.drawable.round_install_mobile_24 - else -> null - }?.let { painterResource(id = it) } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun Toolbar() { - TopAppBar( - title = { Text(text = "Application updates") }, - navigationIcon = { - IconButton( - onClick = { requireActivity().onBackPressedDispatcher.onBackPressed() } - ) { - Icon( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = null, - ) - } - } - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - listenViewModel() - - if (requireArguments().containsKey(ARG_UPDATE_ITEM)) { - val updateItem: UpdateItem = - requireArguments().getParcelableCompat(ARG_UPDATE_ITEM, UpdateItem::class.java) - ?: return - - viewModel.onUpdateItemExists(updateItem) - } else { - viewModel.checkUpdates() - } - } - - private fun listenViewModel() = with(viewModel) { - isNeedToShowChangelogAlert.listenValue(::handleNeedToShowChangelogAlert) - isNeedToShowUnknownSourcesAlert.listenValue(::handleNeedToShowUnknownSourcesAlert) - isNeedToShowIssuesAlert.listenValue(::handleNeedToShowIssuesAlert) - isNeedToShowFileNotFoundAlert.listenValue(::handleNeedToShowFileNotFoundAlert) - } - - private fun handleNeedToShowChangelogAlert(isNeedToShow: Boolean) { - if (isNeedToShow) { - showChangelogAlert() - } - } - - private fun handleNeedToShowUnknownSourcesAlert(isNeedToShow: Boolean) { - if (isNeedToShow) { - showUnknownSourcesAlert() - } - } - - private fun handleNeedToShowIssuesAlert(isNeedToShow: Boolean) { - if (isNeedToShow) { - showIssuesAlert() - } - } - - private fun handleNeedToShowFileNotFoundAlert(isNeedToShow: Boolean) { - if (isNeedToShow) { - showFileNotFoundAlert() - } - } - - private fun showUnknownSourcesAlert() { - context?.showDialog( - title = UiText.Resource(R.string.warning), - message = UiText.Resource(R.string.fragment_updates_unknown_sources_disabled_message), - positiveText = UiText.Resource(R.string.yes), - positiveAction = { AndroidUtils.openInstallUnknownAppsScreen(requireContext()) }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onUnknownSourcesAlertDismissed, - isCancelable = false - ) - } - - private fun showChangelogAlert() { - val messageText = - viewModel.screenState.value.updateItem?.changelog?.ifBlank { - changelogPlaceholder - } ?: changelogPlaceholder - - context?.showDialog( - title = UiText.Resource(R.string.fragment_updates_changelog), - message = UiText.Simple(messageText), - positiveText = UiText.Resource(R.string.ok), - onDismissAction = viewModel::onChangelogAlertDismissed - ) - } - - private fun showIssuesAlert() { - context?.showDialog( - message = UiText.Resource(R.string.fragment_updates_issues_description), - positiveText = UiText.Resource(R.string.action_delete), - positiveAction = viewModel::onIssuesAlertPositiveButtonClicked, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onIssuesAlertDismissed - ) - } - - private fun showFileNotFoundAlert() { - context?.showDialog( - title = UiText.Resource(R.string.warning), - message = UiText.Resource(R.string.fragment_updates_file_not_found_description), - positiveText = UiText.Resource(R.string.ok), - onDismissAction = viewModel::onFileNotFoundAlertDismissed, - isCancelable = false - ) - } - - private fun writeFileToStorage(responseBody: ResponseBody?) { -// if (responseBody == null) return -// -// val updateItem = requireNotNull(viewModel.currentItem.value) -// -// try { -// val destination = requireContext() -// .getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + -// "${File.separator}${updateItem.fileName}.${updateItem.extension}" -// -// val file = File(destination) -// if (file.exists()) file.delete() -// -// var inputStream: InputStream? = null -// var outputStream: OutputStream? = null -// try { -// val fileReader = ByteArray(4096) -// val fileSize: Long = responseBody.contentLength() -// -// requireActivity().runOnUiThread { -// binding.loadingProgress.max = fileSize.toInt() -// binding.loadingProgress.progress = 0 -// } -// -// var fileSizeDownloaded: Long = 0 -// inputStream = responseBody.byteStream() -// outputStream = FileOutputStream(file) -// while (true) { -// val read: Int = inputStream.read(fileReader) -// if (read == -1) { -// break -// } -// outputStream.write(fileReader, 0, read) -// fileSizeDownloaded += read.toLong() -// } -// outputStream.flush() -// -// requireActivity().runOnUiThread { -// installUpdate(file) -// } -// } catch (e: IOException) { -// -// } finally { -// inputStream?.close() -// outputStream?.close() -// } -// } catch (e: IOException) { -// -// } - } - - companion object { - private const val ARG_UPDATE_ITEM = "arg_update_item" - private const val ARG_FILE_BASE_PATH = "file://" - - fun newInstance(updateItem: UpdateItem? = null): UpdatesFragment { - val fragment = UpdatesFragment() - if (updateItem != null) { - fragment.arguments = bundleOf(ARG_UPDATE_ITEM to updateItem) - } else { - fragment.arguments = Bundle() - } - - return fragment - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesViewModel.kt deleted file mode 100644 index f45e3ba6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesViewModel.kt +++ /dev/null @@ -1,375 +0,0 @@ -package com.meloda.fast.screens.updates - -import android.app.DownloadManager -import android.content.Context -import android.content.IntentFilter -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.util.Log -import androidx.core.content.ContextCompat -import androidx.lifecycle.viewModelScope -import com.meloda.fast.R -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.common.AppConstants -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.UpdateManager -import com.meloda.fast.common.UpdateManagerState -import com.meloda.fast.ext.createTimerFlow -import com.meloda.fast.ext.isSdkAtLeast -import com.meloda.fast.ext.listenValue -import com.meloda.fast.model.UpdateItem -import com.meloda.fast.receiver.DownloadManagerReceiver -import com.meloda.fast.screens.updates.model.UpdateState -import com.meloda.fast.screens.updates.model.UpdatesScreenState -import com.meloda.fast.util.AndroidUtils -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* -import java.io.File -import kotlin.math.roundToInt -import kotlin.time.Duration.Companion.milliseconds - -interface UpdatesViewModel { - val screenState: MutableStateFlow - val currentDownloadProgress: StateFlow - - val isNeedToShowChangelogAlert: Flow - val isNeedToShowUnknownSourcesAlert: Flow - val isNeedToShowIssuesAlert: Flow - val isNeedToShowFileNotFoundAlert: Flow - - fun onUpdateItemExists(updateItem: UpdateItem) - - fun checkUpdates() - - fun onChangelogButtonClicked() - fun onActionButtonClicked() - fun onCancelDownloadButtonClicked() - fun onIssuesButtonClicked() - - fun onChangelogAlertDismissed() - fun onUnknownSourcesAlertDismissed() - fun onIssuesAlertDismissed() - fun onIssuesAlertPositiveButtonClicked() - fun onFileNotFoundAlertDismissed() -} - -class UpdatesViewModelImpl constructor( - private val updateManager: UpdateManager, -) : DeprecatedBaseViewModel(), UpdatesViewModel { - - override val screenState = MutableStateFlow(UpdatesScreenState.EMPTY) - override val currentDownloadProgress = MutableStateFlow(0) - - override val isNeedToShowChangelogAlert = MutableStateFlow(false) - override val isNeedToShowUnknownSourcesAlert = MutableStateFlow(false) - override val isNeedToShowIssuesAlert = MutableStateFlow(false) - override val isNeedToShowFileNotFoundAlert = MutableStateFlow(false) - - private var currentJob: Job? = null - - private val downloadManager by lazy { - AppGlobal.Instance.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - } - - init { - updateManager.stateFlow.listenValue(::updateState) - } - - override fun onUpdateItemExists(updateItem: UpdateItem) { - val newForm = screenState.value.copy( - updateItem = updateItem, - updateState = UpdateState.NewUpdate, - error = null - ) - screenState.update { newForm } - } - - override fun checkUpdates() { - if (currentJob != null) { - currentJob?.cancel() - currentJob = null - } - - updateUpdateState(UpdateState.Loading) - - currentJob = updateManager.checkUpdates().apply { - invokeOnCompletion { currentJob = null } - } - } - - override fun onChangelogButtonClicked() { - isNeedToShowChangelogAlert.tryEmit(true) - } - - override fun onActionButtonClicked() { - val state = screenState.value.updateState - - if (!state.isDownloaded()) { - downloadUpdate() - return - } - - when (state) { - UpdateState.NewUpdate -> checkIsInstallingAllowed() - UpdateState.NoUpdates, UpdateState.Error -> checkUpdates() - UpdateState.Downloaded -> installUpdate() - else -> Unit - } - } - - override fun onCancelDownloadButtonClicked() { - when (screenState.value.updateState) { - UpdateState.Downloading -> cancelCurrentDownload() - else -> Unit - } - } - - override fun onIssuesButtonClicked() { - isNeedToShowIssuesAlert.tryEmit(true) - } - - override fun onChangelogAlertDismissed() { - isNeedToShowChangelogAlert.tryEmit(false) - } - - override fun onUnknownSourcesAlertDismissed() { - isNeedToShowUnknownSourcesAlert.tryEmit(false) - } - - override fun onIssuesAlertDismissed() { - isNeedToShowIssuesAlert.tryEmit(false) - } - - override fun onIssuesAlertPositiveButtonClicked() { - deleteInstalledFile() - checkUpdates() - } - - override fun onFileNotFoundAlertDismissed() { - isNeedToShowFileNotFoundAlert.tryEmit(false) - checkUpdates() - } - - private fun deleteInstalledFile() { - // TODO: 26.03.2023, Danil Nikolaev: use updateItem - val apkName = "bruhLol" - - val apkFileName = "$apkName.apk" - - val destination = "%s/$apkFileName".format( - AppGlobal.Instance.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() - ) - - val file = File(destination) - if (!file.exists()) return - file.delete() - } - - private fun updateState(updateManagerState: UpdateManagerState) { - val item = UpdateItem.EMPTY -// updateManagerState.updateItem - val error = updateManagerState.throwable - - var fileExists = false - - if (item != null) { - // TODO: 26.03.2023, Danil Nikolaev: use updateItem - val apkName = "bruhLol" - - val apkFileName = "$apkName.apk" - - val destination = "%s/$apkFileName".format( - AppGlobal.Instance.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() - ) - - val file = File(destination) - fileExists = file.exists() - } - - val newUpdateState = when { - item != null -> { - if (fileExists) { - UpdateState.Downloaded - } else { - UpdateState.NewUpdate - } - } - error != null -> UpdateState.Error - else -> UpdateState.NoUpdates - } - updateUpdateState(newUpdateState) - - val newError = error?.message - - val newState = screenState.value.copy( - updateItem = item, - error = newError - ) - screenState.update { newState } - } - - private fun checkIsInstallingAllowed() { - if (!isSdkAtLeast(Build.VERSION_CODES.O) && !AndroidUtils.isCanInstallUnknownApps()) { - isNeedToShowUnknownSourcesAlert.update { true } - } else { - downloadUpdate() - } - } - - private var downloadId: Long? = null - - private fun downloadUpdate() { - val context = AppGlobal.Instance - - updateUpdateState(UpdateState.Loading) -// val newUpdate = screenState.value.updateItem ?: return - - // TODO: 26.03.2023, Danil Nikolaev: use updateItem - val apkName = "bruhLol" - - val apkFileName = "$apkName.apk" - - val destination = "%s/$apkFileName".format( - context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() - ) - - val file = File(destination) - if (file.exists()) { - updateUpdateState(UpdateState.Downloaded) - return - } - - val downloadUri = try { - Uri.parse( - "https://vk.com/doc157582555_635903147?hash=gTEOVno21WCtxX9GclYo8Liloat5V4xt4WB6nSuOMl8&dl=PQvcF2f7jyJDhJzMFOfRzCZXMx0MztmnwzhQYe4Ycdz" - ) - } catch (e: Exception) { - e.printStackTrace() - Uri.EMPTY - } - - val request = DownloadManager.Request(downloadUri).apply { - setTitle("${context.getString(R.string.app_name)} $apkFileName") - setMimeType(AppConstants.INSTALL_APP_MIME_TYPE) - setDestinationInExternalFilesDir( - context, - Environment.DIRECTORY_DOWNLOADS, - apkFileName - ) - setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or - DownloadManager.Request.NETWORK_MOBILE - ) - setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - } - - val receiver = DownloadManagerReceiver() - receiver.onReceiveAction = { - downloadId = null - - installUpdate(file) - - context.unregisterReceiver(receiver) - } - - ContextCompat.registerReceiver( - context, - receiver, - IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), - ContextCompat.RECEIVER_NOT_EXPORTED - ) - - downloadId = downloadManager.enqueue(request) - - updateUpdateState(UpdateState.Downloading) - - var isDownloaded = false - - createTimerFlow( - isNeedToEndCondition = { isDownloaded }, - onStartAction = { - Log.d("Downloading update", "downloadUpdate: onStart") - }, - onTickAction = { - val query = DownloadManager.Query() - query.setFilterById(downloadId ?: -1) - - val cursor = downloadManager.query(query) - if (cursor.moveToFirst()) { - val sizeIndex = - cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) - val downloadedIndex = - cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) - val size = cursor.getInt(sizeIndex) - val downloaded = cursor.getInt(downloadedIndex) - val progress = if (size != -1) { - downloaded * 100.0F / size - } else { - 0.0F - } - - val intProgress = progress.roundToInt() - if (intProgress >= 1) { - currentDownloadProgress.emit(intProgress) - } - - Log.d("Downloading update", "progress: $progress%") - - if (intProgress >= 100) { - isDownloaded = true - currentDownloadProgress.emit(0) - updateUpdateState(UpdateState.Downloaded) - } - } - }, - onEndAction = {}, - interval = 250.milliseconds - ).launchIn(viewModelScope) - } - - private fun checkDownloadedFileExists(): File? { - // TODO: 26.03.2023, Danil Nikolaev: use updateItem - val apkName = "bruhLol" - - val apkFileName = "$apkName.apk" - - val destination = "%s/$apkFileName".format( - AppGlobal.Instance.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() - ) - - val file = File(destination) - return if (file.exists()) file else null - } - - private fun installUpdate(file: File? = null) { - val context = AppGlobal.Instance - val destinationFile = file ?: checkDownloadedFileExists() ?: run { - isNeedToShowFileNotFoundAlert.tryEmit(true) - return - } - - val installIntent = AndroidUtils.getInstallPackageIntent( - context, - ARG_PROVIDER_PATH, - destinationFile - ) - - context.startActivity(installIntent) - } - - private fun updateUpdateState(newState: UpdateState) { - val newForm = screenState.value.copy(updateState = newState) - screenState.update { newForm } - } - - private fun cancelCurrentDownload() { - currentDownloadProgress.tryEmit(0) - downloadId?.run { downloadManager.remove(this) } - checkUpdates() - } - - companion object { - private const val ARG_PROVIDER_PATH = ".provider" - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/di/UpdatesDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/di/UpdatesDI.kt deleted file mode 100644 index 60634afb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/di/UpdatesDI.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.updates.di - -import com.meloda.fast.screens.updates.UpdatesViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val updatesModule = module { - viewModelOf(::UpdatesViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdateState.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdateState.kt deleted file mode 100644 index 8ea8b863..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdateState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.screens.updates.model - -sealed class UpdateState { - object NewUpdate : UpdateState() - object NoUpdates : UpdateState() - object Loading : UpdateState() - object Error : UpdateState() - object Downloading : UpdateState() - object Downloaded : UpdateState() - - fun isNewUpdate() = this == NewUpdate - fun isLoading() = this == Loading - fun isDownloading() = this == Downloading - fun isDownloaded() = this == Downloaded -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdatesScreenState.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdatesScreenState.kt deleted file mode 100644 index c99b0dcb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdatesScreenState.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.meloda.fast.screens.updates.model - -import com.meloda.fast.model.UpdateItem - -data class UpdatesScreenState( - val updateItem: UpdateItem?, - val updateState: UpdateState, - val error: String?, - val currentProgress: Float?, - val isProgressIntermediate: Boolean, -) { - - companion object { - val EMPTY = UpdatesScreenState( - updateItem = null, - updateState = UpdateState.NoUpdates, - error = null, - currentProgress = null, - isProgressIntermediate = true - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/userbanned/UserBannedFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/userbanned/UserBannedFragment.kt deleted file mode 100644 index 49930a03..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/userbanned/UserBannedFragment.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.meloda.fast.screens.userbanned - -import android.os.Bundle -import android.view.View -import androidx.core.os.bundleOf -import by.kirich1409.viewbindingdelegate.viewBinding -import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.databinding.FragmentUserBannedBinding -import dev.chrisbanes.insetter.applyInsetter - -class UserBannedFragment : BaseFragment(R.layout.fragment_user_banned) { - - companion object { - - private const val ArgMemberName = "member_name" - private const val ArgMessage = "message" - private const val ArgRestoreUrl = "restore_url" - private const val ArgAccessToken = "access_token" - - fun newInstance( - memberName: String, - message: String, - restoreUrl: String, - accessToken: String - ): UserBannedFragment { - val fragment = UserBannedFragment() - fragment.arguments = bundleOf( - ArgMemberName to memberName, - ArgMessage to message, - ArgRestoreUrl to restoreUrl, - ArgAccessToken to accessToken - ) - return fragment - } - } - - private val binding by viewBinding(FragmentUserBannedBinding::bind) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.root.applyInsetter { - type(navigationBars = true) { padding() } - } - - binding.toolbar.applyInsetter { - type(statusBars = true) { padding() } - } - binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } - - binding.name.text = requireArguments().getString(ArgMemberName) - binding.reason.text = requireArguments().getString(ArgMessage) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/service/LongPollQSTileService.kt b/app/src/main/kotlin/com/meloda/fast/service/LongPollQSTileService.kt deleted file mode 100644 index bb033821..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/LongPollQSTileService.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.meloda.fast.service - -import android.content.Intent -import android.service.quicksettings.Tile -import android.service.quicksettings.TileService -import android.util.Log -import com.meloda.fast.screens.main.activity.MainActivity - -class LongPollQSTileService : TileService() { - - override fun onTileAdded() { - Log.d("LongPollQSTileService", "onTileAdded") - super.onTileAdded() - } - - override fun onStartListening() { - qsTile.state = Tile.STATE_ACTIVE - qsTile.updateTile() - Log.d("LongPollQSTileService", "onStartListening") - super.onStartListening() - } - - - override fun onStopListening() { - Log.d("LongPollQSTileService", "onStopListening") - super.onStopListening() - } - - override fun onClick() { - Log.d("LongPollQSTileService", "onClick") - - startActivityAndCollapse(Intent(this, MainActivity::class.java).apply { - putExtra("data", "open_settings") - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }) - super.onClick() - } - - override fun onTileRemoved() { - Log.d("LongPollQSTileService", "onTileRemoved") - super.onTileRemoved() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt b/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt deleted file mode 100644 index 26fb7c0b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt +++ /dev/null @@ -1,281 +0,0 @@ -package com.meloda.fast.service - -import android.app.Notification -import android.app.PendingIntent -import android.app.Service -import android.content.Intent -import android.os.IBinder -import android.util.Log -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.content.edit -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import com.meloda.fast.R -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.api.model.base.BaseVkLongPoll -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest -import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.data.messages.MessagesRepository -import com.meloda.fast.ext.isTrue -import com.meloda.fast.receiver.StopLongPollServiceReceiver -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.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 - -class LongPollService : Service() { - - companion object { - const val TAG = "LongPollTask" - - const val KeyLongPollWasDestroyed = "long_poll_was_destroyed" - - private const val NOTIFICATION_ID = 1001 - } - - private val job = SupervisorJob() - - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.d(TAG, "error: $throwable") - throwable.printStackTrace() - } - - private val coroutineContext: CoroutineContext - get() = Dispatchers.IO + job + exceptionHandler - - private val coroutineScope = CoroutineScope(coroutineContext) - - private val repository: MessagesRepository by inject() - - private val updatesParser: LongPollUpdatesParser by inject() - - private var asForeground = true - private var foregroundNotification: Notification? = null - - override fun onCreate() { - super.onCreate() - - if (AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - SettingsFragment.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND - ) - ) { - val notificationBuilder = - NotificationsUtils.createNotification( - context = this, - title = "LongPoll", - contentText = "обновление ваших сообщений в фоне", - notRemovable = false, - channelId = "long_polling", - priority = NotificationsUtils.NotificationPriority.Low, - category = NotificationCompat.CATEGORY_SERVICE, - customNotificationId = NOTIFICATION_ID - ) - - foregroundNotification = notificationBuilder.build() - startForeground(NOTIFICATION_ID, foregroundNotification) - } - } - - override fun onBind(p0: Intent?): IBinder? { - return null - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - asForeground = intent?.getBooleanExtra("foreground", false).isTrue - - Log.d( - "LongPollService", - "onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId" - ) - - coroutineScope.launch { startPolling().join() } - - val stopIntent = Intent(this, StopLongPollServiceReceiver::class.java).apply { - action = StopLongPollServiceReceiver.ACTION_STOP - putExtra(StopLongPollServiceReceiver.NOTIFICATION_ID, startId) - } - val stopPendingIntent = PendingIntent.getBroadcast( - this, - 1, - stopIntent, - PendingIntent.FLAG_IMMUTABLE - ) - - val action = NotificationCompat.Action( - R.drawable.ic_round_close_24, - getString(R.string.action_stop), - stopPendingIntent - ) - - if (asForeground) { - val notificationBuilder = - NotificationsUtils.createNotification( - context = this, - title = "LongPoll", - contentText = "обновление ваших сообщений в фоне", - notRemovable = false, - channelId = "long_polling", - priority = NotificationsUtils.NotificationPriority.Low, - category = NotificationCompat.CATEGORY_SERVICE, - actions = listOf(action), - customNotificationId = NOTIFICATION_ID - ) - - foregroundNotification = notificationBuilder.build() - - startForeground(NOTIFICATION_ID, foregroundNotification) - } else { - if (foregroundNotification != null) { - NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID) - foregroundNotification = null - } - } - return START_STICKY - } - - private fun startPolling(): Job { - if (job.isCompleted || job.isCancelled) { - Log.d("LongPollService", "job is completed or cancelled. Fuck off") - throw Exception("Job is over") - } - - Log.d("LongPollService", "job started") - - return coroutineScope.launch { - var serverInfo = getServerInfo() - ?: throw ApiError(errorMessage = "bad VK response (server info)") - - var lastUpdatesResponse: JsonObject? = getUpdatesResponse(serverInfo) - ?: throw ApiError(errorMessage = "initiation error: bad VK response (last updates)") - - var failCount = 0 - - while (job.isActive) { - if (lastUpdatesResponse == null) { - failCount++ - serverInfo = getServerInfo() - ?: throw ApiError(errorMessage = "failed retrieving server info after error: bad VK response (server info #2)") - lastUpdatesResponse = getUpdatesResponse(serverInfo) - continue - } - - when (lastUpdatesResponse["failed"]?.asInt) { - 1 -> { - var newTs = lastUpdatesResponse["ts"]?.asInt - if (newTs == null) { - newTs = serverInfo.ts - failCount++ - } - - lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) - } - - 2, 3 -> { - serverInfo = getServerInfo() - ?: throw ApiError( - errorMessage = "failed retrieving server info after error: bad VK response (server info #3)" - ) - lastUpdatesResponse = getUpdatesResponse(serverInfo) - } - - else -> { - val newTs = lastUpdatesResponse["ts"]?.asInt - - if (newTs == null) { - failCount++ - } else { - val updates = lastUpdatesResponse["updates"]?.asJsonArray - - if (updates == null) { - failCount++ - } else { - updates.forEach { item -> - item.asJsonArray?.also { - launch { - handleUpdateEvent(it) - } - } ?: failCount++ - } - } - - lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) - } - } - } - } - } - } - - private suspend fun getServerInfo(): BaseVkLongPoll? { - val response = repository.getLongPollServer( - MessagesGetLongPollServerRequest( - needPts = true, - version = VKConstants.LP_VERSION - ) - ) - - println("$TAG: serverInfoResponse: $response") - - if (response is ApiAnswer.Error) return null - if (response is ApiAnswer.Success) { - return response.data.response - } - - return null - } - - private suspend fun getUpdatesResponse(server: BaseVkLongPoll): JsonObject? { - val response = repository.getLongPollUpdates( - serverUrl = "https://${server.server}", - params = LongPollGetUpdatesRequest( - key = server.key, - ts = server.ts, - wait = 25, - mode = 2 or 8 or 32 or 64 or 128, - version = VKConstants.LP_VERSION - ) - ) - - println("$TAG: lastUpdateResponse: $response") - - if (response is ApiAnswer.Success) { - return response.data - } - - return null - } - - private fun handleUpdateEvent(eventJson: JsonArray) { - updatesParser.parseNextUpdate(eventJson) - } - - override fun onDestroy() { - Log.d("LongPollService", "onDestroy") - try { - AppGlobal.preferences.edit { - putBoolean(KeyLongPollWasDestroyed, true) - } - job.cancel() - } catch (e: Exception) { - e.printStackTrace() - } - super.onDestroy() - } - - override fun onLowMemory() { - Log.d("LongPollService", "onLowMemory") - super.onLowMemory() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/service/MyCustomControlService.kt b/app/src/main/kotlin/com/meloda/fast/service/MyCustomControlService.kt deleted file mode 100644 index 9e20b4e0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/MyCustomControlService.kt +++ /dev/null @@ -1,59 +0,0 @@ -@file:RequiresApi(Build.VERSION_CODES.R) - -package com.meloda.fast.service - -import android.app.PendingIntent -import android.content.Intent -import android.os.Build -import android.provider.AlarmClock.EXTRA_MESSAGE -import android.service.controls.Control -import android.service.controls.ControlsProviderService -import android.service.controls.DeviceTypes -import android.service.controls.actions.ControlAction -import androidx.annotation.RequiresApi -import com.meloda.fast.screens.main.activity.MainActivity -import kotlinx.coroutines.jdk9.flowPublish -import java.util.concurrent.Flow -import java.util.function.Consumer - -private const val LIGHT_ID = 1234 -private const val LIGHT_TITLE = "Enable Long Polling" -private const val LIGHT_TYPE = DeviceTypes.TYPE_DOOR - - -class MyCustomControlService : ControlsProviderService() { - - override fun createPublisherForAllAvailable() = - flowPublish { - send(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE)) - } - - private fun createStatelessControl(id: Int, title: String, type: Int): Control { - val intent = Intent(this, MainActivity::class.java) - .putExtra(EXTRA_MESSAGE, title) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val action = PendingIntent.getActivity( - this, - id, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - return Control.StatelessBuilder(id.toString(), action) - .setTitle(title) - .setDeviceType(type) - .build() - } - - override fun createPublisherFor(controlIds: MutableList): Flow.Publisher { - TODO("Not yet implemented") - } - - override fun performControlAction( - controlId: String, - action: ControlAction, - consumer: Consumer - ) { - - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt b/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt deleted file mode 100644 index 29a2580e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.meloda.fast.service - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import android.util.Log -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.network.account.AccountSetOfflineRequest -import com.meloda.fast.api.network.account.AccountSetOnlineRequest -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.data.account.AccountsRepository -import com.meloda.fast.screens.settings.SettingsFragment -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.launch -import org.koin.android.ext.android.inject -import java.util.Timer -import kotlin.concurrent.schedule -import kotlin.coroutines.CoroutineContext - -class OnlineService : Service(), CoroutineScope { - - private val job = SupervisorJob() - - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.d(LongPollService.TAG, "error: $throwable") - throwable.printStackTrace() - } - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Default + job + exceptionHandler - - private val repository: AccountsRepository by inject() - - private var timer: Timer? = null - - private var currentJob: Job? = null - - override fun onBind(intent: Intent?): IBinder? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d("OnlineService", "onStartCommand: flags: $flags; startId: $startId") - - if (AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_VISIBILITY_SEND_ONLINE_STATUS, true - ) - ) { - createTimer() - } - - return START_STICKY_COMPATIBILITY - } - - private fun createTimer() { - timer = Timer().apply { - schedule(delay = 0, period = 300 * 1000L) { - setOnline() - } - } - } - - private fun setOnline() { - if (currentJob != null) return - - currentJob = launch { - Log.d("OnlineService", "setOnline()") - - val token = UserConfig.fastToken ?: UserConfig.accessToken - - if (token.isBlank()) { - Log.d("OnlineService", "setOnline: token is empty") - return@launch - } - - val response = repository.setOnline( - AccountSetOnlineRequest( - voip = false, - accessToken = token - ) - ) - Log.d("OnlineService", "setOnline: response: $response") - currentJob = null - } - } - - private suspend fun setOffline() { - Log.d("OnlineService", "setOffline()") - val response = repository.setOffline( - AccountSetOfflineRequest( - accessToken = UserConfig.accessToken - ) - ) - Log.d("OnlineService", "setOffline: response: $response") - } - - override fun onDestroy() { - super.onDestroy() - timer?.cancel() - currentJob?.cancel("OnlineService destroyed") - Log.d("OnlineService", "onDestroy") - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/AppTheme.kt b/app/src/main/kotlin/com/meloda/fast/ui/AppTheme.kt deleted file mode 100644 index a9f2282d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/AppTheme.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.meloda.fast.ui - -import android.os.Build -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import com.meloda.fast.R -import com.meloda.fast.ext.isSystemUsingDarkMode -import com.meloda.fast.ext.isUsingDarkTheme -import com.meloda.fast.ext.isUsingDynamicColors - - -val StandardColorScheme - get() = if (isSystemUsingDarkMode()) DarkColorScheme - else LightColorScheme - -private val LightColorScheme = lightColorScheme() -private val DarkColorScheme = darkColorScheme() - -@Composable -fun dynamicColorScheme(): ColorScheme { - val context = LocalContext.current - return if (isSystemUsingDarkMode()) dynamicDarkColorScheme(context) - else dynamicLightColorScheme(context) -} - -private val googleSansFonts = FontFamily( - Font(R.font.google_sans_regular), - Font(R.font.google_sans_italic, style = FontStyle.Italic), - Font(R.font.google_sans_medium, weight = FontWeight.Medium), - Font( - R.font.google_sans_medium_italic, - weight = FontWeight.Medium, - style = FontStyle.Italic - ), - Font(R.font.google_sans_bold, weight = FontWeight.Bold), - Font( - R.font.google_sans_bold_italic, - weight = FontWeight.Bold, - style = FontStyle.Italic - ), -) - -private val robotoFonts = FontFamily( - Font(R.font.roboto_regular), - // TODO: 27.03.2023, Danil Nikolaev: add all roboto fonts -) - -@Composable -fun AppTheme( - predefinedColorScheme: ColorScheme? = null, - useDarkTheme: Boolean = isUsingDarkTheme(), - useDynamicColors: Boolean = isUsingDynamicColors(), - content: @Composable () -> Unit -) { - val colorScheme: ColorScheme = when { - useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (useDarkTheme) dynamicDarkColorScheme(context) - else dynamicLightColorScheme(context) - } - - useDarkTheme -> DarkColorScheme - else -> LightColorScheme - } - - val typography = MaterialTheme.typography.copy( - displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = googleSansFonts), - displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = googleSansFonts), - displaySmall = MaterialTheme.typography.displaySmall.copy(fontFamily = googleSansFonts), - headlineLarge = MaterialTheme.typography.headlineLarge.copy(fontFamily = googleSansFonts), - headlineMedium = MaterialTheme.typography.headlineMedium.copy(fontFamily = googleSansFonts), - headlineSmall = MaterialTheme.typography.headlineSmall.copy(fontFamily = googleSansFonts), - bodyLarge = MaterialTheme.typography.bodyLarge.copy(fontFamily = robotoFonts), - bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontFamily = robotoFonts), - bodySmall = MaterialTheme.typography.bodySmall.copy(fontFamily = robotoFonts) - ) - - MaterialTheme( - colorScheme = predefinedColorScheme ?: colorScheme, - typography = typography, - content = content - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/BlueColorScheme.kt b/app/src/main/kotlin/com/meloda/fast/ui/BlueColorScheme.kt deleted file mode 100644 index 12f27494..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/BlueColorScheme.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.meloda.fast.ui - -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import com.meloda.fast.ext.isSystemUsingDarkMode -import com.meloda.fast.ui.colors.Blue - -val BlueColorScheme - get() = if (isSystemUsingDarkMode()) BlueDarkColorScheme - else BlueLightColorScheme - -val BlueLightColorScheme = lightColorScheme( - primary = Blue.md_theme_light_primary, - onPrimary = Blue.md_theme_light_onPrimary, - primaryContainer = Blue.md_theme_light_primaryContainer, - onPrimaryContainer = Blue.md_theme_light_onPrimaryContainer, - secondary = Blue.md_theme_light_secondary, - onSecondary = Blue.md_theme_light_onSecondary, - secondaryContainer = Blue.md_theme_light_secondaryContainer, - onSecondaryContainer = Blue.md_theme_light_onSecondaryContainer, - tertiary = Blue.md_theme_light_tertiary, - onTertiary = Blue.md_theme_light_onTertiary, - tertiaryContainer = Blue.md_theme_light_tertiaryContainer, - onTertiaryContainer = Blue.md_theme_light_onTertiaryContainer, - error = Blue.md_theme_light_error, - errorContainer = Blue.md_theme_light_errorContainer, - onError = Blue.md_theme_light_onError, - onErrorContainer = Blue.md_theme_light_onErrorContainer, - background = Blue.md_theme_light_background, - onBackground = Blue.md_theme_light_onBackground, - surface = Blue.md_theme_light_surface, - onSurface = Blue.md_theme_light_onSurface, - surfaceVariant = Blue.md_theme_light_surfaceVariant, - onSurfaceVariant = Blue.md_theme_light_onSurfaceVariant, - outline = Blue.md_theme_light_outline, - inverseOnSurface = Blue.md_theme_light_inverseOnSurface, - inverseSurface = Blue.md_theme_light_inverseSurface, - inversePrimary = Blue.md_theme_light_inversePrimary, - surfaceTint = Blue.md_theme_light_surfaceTint, - outlineVariant = Blue.md_theme_light_outlineVariant, - scrim = Blue.md_theme_light_scrim, -) - -val BlueDarkColorScheme = darkColorScheme( - primary = Blue.md_theme_dark_primary, - onPrimary = Blue.md_theme_dark_onPrimary, - primaryContainer = Blue.md_theme_dark_primaryContainer, - onPrimaryContainer = Blue.md_theme_dark_onPrimaryContainer, - secondary = Blue.md_theme_dark_secondary, - onSecondary = Blue.md_theme_dark_onSecondary, - secondaryContainer = Blue.md_theme_dark_secondaryContainer, - onSecondaryContainer = Blue.md_theme_dark_onSecondaryContainer, - tertiary = Blue.md_theme_dark_tertiary, - onTertiary = Blue.md_theme_dark_onTertiary, - tertiaryContainer = Blue.md_theme_dark_tertiaryContainer, - onTertiaryContainer = Blue.md_theme_dark_onTertiaryContainer, - error = Blue.md_theme_dark_error, - errorContainer = Blue.md_theme_dark_errorContainer, - onError = Blue.md_theme_dark_onError, - onErrorContainer = Blue.md_theme_dark_onErrorContainer, - background = Blue.md_theme_dark_background, - onBackground = Blue.md_theme_dark_onBackground, - surface = Blue.md_theme_dark_surface, - onSurface = Blue.md_theme_dark_onSurface, - surfaceVariant = Blue.md_theme_dark_surfaceVariant, - onSurfaceVariant = Blue.md_theme_dark_onSurfaceVariant, - outline = Blue.md_theme_dark_outline, - inverseOnSurface = Blue.md_theme_dark_inverseOnSurface, - inverseSurface = Blue.md_theme_dark_inverseSurface, - inversePrimary = Blue.md_theme_dark_inversePrimary, - surfaceTint = Blue.md_theme_dark_surfaceTint, - outlineVariant = Blue.md_theme_dark_outlineVariant, - scrim = Blue.md_theme_dark_scrim, -) diff --git a/app/src/main/kotlin/com/meloda/fast/ui/GreenColorScheme.kt b/app/src/main/kotlin/com/meloda/fast/ui/GreenColorScheme.kt deleted file mode 100644 index 79e9dc77..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/GreenColorScheme.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.meloda.fast.ui - -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import com.meloda.fast.ext.isSystemUsingDarkMode -import com.meloda.fast.ui.colors.Green - -val GreenColorScheme - get() = if (isSystemUsingDarkMode()) GreenDarkColorScheme - else GreenLightColorScheme - -val GreenLightColorScheme = lightColorScheme( - primary = Green.md_theme_light_primary, - onPrimary = Green.md_theme_light_onPrimary, - primaryContainer = Green.md_theme_light_primaryContainer, - onPrimaryContainer = Green.md_theme_light_onPrimaryContainer, - secondary = Green.md_theme_light_secondary, - onSecondary = Green.md_theme_light_onSecondary, - secondaryContainer = Green.md_theme_light_secondaryContainer, - onSecondaryContainer = Green.md_theme_light_onSecondaryContainer, - tertiary = Green.md_theme_light_tertiary, - onTertiary = Green.md_theme_light_onTertiary, - tertiaryContainer = Green.md_theme_light_tertiaryContainer, - onTertiaryContainer = Green.md_theme_light_onTertiaryContainer, - error = Green.md_theme_light_error, - errorContainer = Green.md_theme_light_errorContainer, - onError = Green.md_theme_light_onError, - onErrorContainer = Green.md_theme_light_onErrorContainer, - background = Green.md_theme_light_background, - onBackground = Green.md_theme_light_onBackground, - surface = Green.md_theme_light_surface, - onSurface = Green.md_theme_light_onSurface, - surfaceVariant = Green.md_theme_light_surfaceVariant, - onSurfaceVariant = Green.md_theme_light_onSurfaceVariant, - outline = Green.md_theme_light_outline, - inverseOnSurface = Green.md_theme_light_inverseOnSurface, - inverseSurface = Green.md_theme_light_inverseSurface, - inversePrimary = Green.md_theme_light_inversePrimary, - surfaceTint = Green.md_theme_light_surfaceTint, - outlineVariant = Green.md_theme_light_outlineVariant, - scrim = Green.md_theme_light_scrim, -) - -val GreenDarkColorScheme = darkColorScheme( - primary = Green.md_theme_dark_primary, - onPrimary = Green.md_theme_dark_onPrimary, - primaryContainer = Green.md_theme_dark_primaryContainer, - onPrimaryContainer = Green.md_theme_dark_onPrimaryContainer, - secondary = Green.md_theme_dark_secondary, - onSecondary = Green.md_theme_dark_onSecondary, - secondaryContainer = Green.md_theme_dark_secondaryContainer, - onSecondaryContainer = Green.md_theme_dark_onSecondaryContainer, - tertiary = Green.md_theme_dark_tertiary, - onTertiary = Green.md_theme_dark_onTertiary, - tertiaryContainer = Green.md_theme_dark_tertiaryContainer, - onTertiaryContainer = Green.md_theme_dark_onTertiaryContainer, - error = Green.md_theme_dark_error, - errorContainer = Green.md_theme_dark_errorContainer, - onError = Green.md_theme_dark_onError, - onErrorContainer = Green.md_theme_dark_onErrorContainer, - background = Green.md_theme_dark_background, - onBackground = Green.md_theme_dark_onBackground, - surface = Green.md_theme_dark_surface, - onSurface = Green.md_theme_dark_onSurface, - surfaceVariant = Green.md_theme_dark_surfaceVariant, - onSurfaceVariant = Green.md_theme_dark_onSurfaceVariant, - outline = Green.md_theme_dark_outline, - inverseOnSurface = Green.md_theme_dark_inverseOnSurface, - inverseSurface = Green.md_theme_dark_inverseSurface, - inversePrimary = Green.md_theme_dark_inversePrimary, - surfaceTint = Green.md_theme_dark_surfaceTint, - outlineVariant = Green.md_theme_dark_outlineVariant, - scrim = Green.md_theme_dark_scrim, -) diff --git a/app/src/main/kotlin/com/meloda/fast/ui/RedColorScheme.kt b/app/src/main/kotlin/com/meloda/fast/ui/RedColorScheme.kt deleted file mode 100644 index f270a59f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/RedColorScheme.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.meloda.fast.ui - -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import com.meloda.fast.ext.isSystemUsingDarkMode -import com.meloda.fast.ui.colors.Red - -val RedColorScheme - get() = if (isSystemUsingDarkMode()) RedDarkColorScheme - else RedLightColorScheme - -val RedLightColorScheme = lightColorScheme( - primary = Red.md_theme_light_primary, - onPrimary = Red.md_theme_light_onPrimary, - primaryContainer = Red.md_theme_light_primaryContainer, - onPrimaryContainer = Red.md_theme_light_onPrimaryContainer, - secondary = Red.md_theme_light_secondary, - onSecondary = Red.md_theme_light_onSecondary, - secondaryContainer = Red.md_theme_light_secondaryContainer, - onSecondaryContainer = Red.md_theme_light_onSecondaryContainer, - tertiary = Red.md_theme_light_tertiary, - onTertiary = Red.md_theme_light_onTertiary, - tertiaryContainer = Red.md_theme_light_tertiaryContainer, - onTertiaryContainer = Red.md_theme_light_onTertiaryContainer, - error = Red.md_theme_light_error, - errorContainer = Red.md_theme_light_errorContainer, - onError = Red.md_theme_light_onError, - onErrorContainer = Red.md_theme_light_onErrorContainer, - background = Red.md_theme_light_background, - onBackground = Red.md_theme_light_onBackground, - surface = Red.md_theme_light_surface, - onSurface = Red.md_theme_light_onSurface, - surfaceVariant = Red.md_theme_light_surfaceVariant, - onSurfaceVariant = Red.md_theme_light_onSurfaceVariant, - outline = Red.md_theme_light_outline, - inverseOnSurface = Red.md_theme_light_inverseOnSurface, - inverseSurface = Red.md_theme_light_inverseSurface, - inversePrimary = Red.md_theme_light_inversePrimary, - surfaceTint = Red.md_theme_light_surfaceTint, - outlineVariant = Red.md_theme_light_outlineVariant, - scrim = Red.md_theme_light_scrim, -) - -val RedDarkColorScheme = darkColorScheme( - primary = Red.md_theme_dark_primary, - onPrimary = Red.md_theme_dark_onPrimary, - primaryContainer = Red.md_theme_dark_primaryContainer, - onPrimaryContainer = Red.md_theme_dark_onPrimaryContainer, - secondary = Red.md_theme_dark_secondary, - onSecondary = Red.md_theme_dark_onSecondary, - secondaryContainer = Red.md_theme_dark_secondaryContainer, - onSecondaryContainer = Red.md_theme_dark_onSecondaryContainer, - tertiary = Red.md_theme_dark_tertiary, - onTertiary = Red.md_theme_dark_onTertiary, - tertiaryContainer = Red.md_theme_dark_tertiaryContainer, - onTertiaryContainer = Red.md_theme_dark_onTertiaryContainer, - error = Red.md_theme_dark_error, - errorContainer = Red.md_theme_dark_errorContainer, - onError = Red.md_theme_dark_onError, - onErrorContainer = Red.md_theme_dark_onErrorContainer, - background = Red.md_theme_dark_background, - onBackground = Red.md_theme_dark_onBackground, - surface = Red.md_theme_dark_surface, - onSurface = Red.md_theme_dark_onSurface, - surfaceVariant = Red.md_theme_dark_surfaceVariant, - onSurfaceVariant = Red.md_theme_dark_onSurfaceVariant, - outline = Red.md_theme_dark_outline, - inverseOnSurface = Red.md_theme_dark_inverseOnSurface, - inverseSurface = Red.md_theme_dark_inverseSurface, - inversePrimary = Red.md_theme_dark_inversePrimary, - surfaceTint = Red.md_theme_dark_surfaceTint, - outlineVariant = Red.md_theme_dark_outlineVariant, - scrim = Red.md_theme_dark_scrim, -) diff --git a/app/src/main/kotlin/com/meloda/fast/ui/colors/Blue.kt b/app/src/main/kotlin/com/meloda/fast/ui/colors/Blue.kt deleted file mode 100644 index 6d37c3e8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/colors/Blue.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.meloda.fast.ui.colors - -import androidx.compose.ui.graphics.Color - -object Blue { - val md_theme_light_primary = Color(0xFF1059C6) - val md_theme_light_onPrimary = Color(0xFFFFFFFF) - val md_theme_light_primaryContainer = Color(0xFFD9E2FF) - val md_theme_light_onPrimaryContainer = Color(0xFF001945) - val md_theme_light_secondary = Color(0xFF6F4DA0) - val md_theme_light_onSecondary = Color(0xFFFFFFFF) - val md_theme_light_secondaryContainer = Color(0xFFEDDCFF) - val md_theme_light_onSecondaryContainer = Color(0xFF280056) - val md_theme_light_tertiary = Color(0xFF725572) - val md_theme_light_onTertiary = Color(0xFFFFFFFF) - val md_theme_light_tertiaryContainer = Color(0xFFFDD7FA) - val md_theme_light_onTertiaryContainer = Color(0xFF2A132C) - val md_theme_light_error = Color(0xFFBA1A1A) - val md_theme_light_errorContainer = Color(0xFFFFDAD6) - val md_theme_light_onError = Color(0xFFFFFFFF) - val md_theme_light_onErrorContainer = Color(0xFF410002) - val md_theme_light_background = Color(0xFFFEFBFF) - val md_theme_light_onBackground = Color(0xFF1B1B1F) - val md_theme_light_surface = Color(0xFFFEFBFF) - val md_theme_light_onSurface = Color(0xFF1B1B1F) - val md_theme_light_surfaceVariant = Color(0xFFE1E2EC) - val md_theme_light_onSurfaceVariant = Color(0xFF44464F) - val md_theme_light_outline = Color(0xFF757780) - val md_theme_light_inverseOnSurface = Color(0xFFF2F0F4) - val md_theme_light_inverseSurface = Color(0xFF303034) - val md_theme_light_inversePrimary = Color(0xFFB0C6FF) - val md_theme_light_shadow = Color(0xFF000000) - val md_theme_light_surfaceTint = Color(0xFF1059C6) - val md_theme_light_outlineVariant = Color(0xFFC5C6D0) - val md_theme_light_scrim = Color(0xFF000000) - - val md_theme_dark_primary = Color(0xFFB0C6FF) - val md_theme_dark_onPrimary = Color(0xFF002D6E) - val md_theme_dark_primaryContainer = Color(0xFF00429B) - val md_theme_dark_onPrimaryContainer = Color(0xFFD9E2FF) - val md_theme_dark_secondary = Color(0xFFD7BAFF) - val md_theme_dark_onSecondary = Color(0xFF3F1C6E) - val md_theme_dark_secondaryContainer = Color(0xFF563587) - val md_theme_dark_onSecondaryContainer = Color(0xFFEDDCFF) - val md_theme_dark_tertiary = Color(0xFFE0BBDE) - val md_theme_dark_onTertiary = Color(0xFF412742) - val md_theme_dark_tertiaryContainer = Color(0xFF593D5A) - val md_theme_dark_onTertiaryContainer = Color(0xFFFDD7FA) - val md_theme_dark_error = Color(0xFFFFB4AB) - val md_theme_dark_errorContainer = Color(0xFF93000A) - val md_theme_dark_onError = Color(0xFF690005) - val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) - val md_theme_dark_background = Color(0xFF1B1B1F) - val md_theme_dark_onBackground = Color(0xFFE3E2E6) - val md_theme_dark_surface = Color(0xFF1B1B1F) - val md_theme_dark_onSurface = Color(0xFFE3E2E6) - val md_theme_dark_surfaceVariant = Color(0xFF44464F) - val md_theme_dark_onSurfaceVariant = Color(0xFFC5C6D0) - val md_theme_dark_outline = Color(0xFF8F9099) - val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F) - val md_theme_dark_inverseSurface = Color(0xFFE3E2E6) - val md_theme_dark_inversePrimary = Color(0xFF1059C6) - val md_theme_dark_shadow = Color(0xFF000000) - val md_theme_dark_surfaceTint = Color(0xFFB0C6FF) - val md_theme_dark_outlineVariant = Color(0xFF44464F) - val md_theme_dark_scrim = Color(0xFF000000) - - - val seed = Color(0xFF3771DF) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/colors/Green.kt b/app/src/main/kotlin/com/meloda/fast/ui/colors/Green.kt deleted file mode 100644 index f21e7c4d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/colors/Green.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.meloda.fast.ui.colors - -import androidx.compose.ui.graphics.Color - -object Green { - val md_theme_light_primary = Color(0xFF006E29) - val md_theme_light_onPrimary = Color(0xFFFFFFFF) - val md_theme_light_primaryContainer = Color(0xFF94F99D) - val md_theme_light_onPrimaryContainer = Color(0xFF002107) - val md_theme_light_secondary = Color(0xFF516350) - val md_theme_light_onSecondary = Color(0xFFFFFFFF) - val md_theme_light_secondaryContainer = Color(0xFFD4E8D0) - val md_theme_light_onSecondaryContainer = Color(0xFF0F1F10) - val md_theme_light_tertiary = Color(0xFF39656C) - val md_theme_light_onTertiary = Color(0xFFFFFFFF) - val md_theme_light_tertiaryContainer = Color(0xFFBCEAF2) - val md_theme_light_onTertiaryContainer = Color(0xFF001F24) - val md_theme_light_error = Color(0xFFBA1A1A) - val md_theme_light_errorContainer = Color(0xFFFFDAD6) - val md_theme_light_onError = Color(0xFFFFFFFF) - val md_theme_light_onErrorContainer = Color(0xFF410002) - val md_theme_light_background = Color(0xFFFCFDF7) - val md_theme_light_onBackground = Color(0xFF1A1C19) - val md_theme_light_surface = Color(0xFFFCFDF7) - val md_theme_light_onSurface = Color(0xFF1A1C19) - val md_theme_light_surfaceVariant = Color(0xFFDEE5D9) - val md_theme_light_onSurfaceVariant = Color(0xFF424940) - val md_theme_light_outline = Color(0xFF727970) - val md_theme_light_inverseOnSurface = Color(0xFFF0F1EB) - val md_theme_light_inverseSurface = Color(0xFF2F312D) - val md_theme_light_inversePrimary = Color(0xFF79DC84) - val md_theme_light_shadow = Color(0xFF000000) - val md_theme_light_surfaceTint = Color(0xFF006E29) - val md_theme_light_outlineVariant = Color(0xFFC2C9BE) - val md_theme_light_scrim = Color(0xFF000000) - - val md_theme_dark_primary = Color(0xFF79DC84) - val md_theme_dark_onPrimary = Color(0xFF003911) - val md_theme_dark_primaryContainer = Color(0xFF00531D) - val md_theme_dark_onPrimaryContainer = Color(0xFF94F99D) - val md_theme_dark_secondary = Color(0xFFB8CCB5) - val md_theme_dark_onSecondary = Color(0xFF243424) - val md_theme_dark_secondaryContainer = Color(0xFF3A4B3A) - val md_theme_dark_onSecondaryContainer = Color(0xFFD4E8D0) - val md_theme_dark_tertiary = Color(0xFFA1CED6) - val md_theme_dark_onTertiary = Color(0xFF00363D) - val md_theme_dark_tertiaryContainer = Color(0xFF1F4D54) - val md_theme_dark_onTertiaryContainer = Color(0xFFBCEAF2) - val md_theme_dark_error = Color(0xFFFFB4AB) - val md_theme_dark_errorContainer = Color(0xFF93000A) - val md_theme_dark_onError = Color(0xFF690005) - val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) - val md_theme_dark_background = Color(0xFF1A1C19) - val md_theme_dark_onBackground = Color(0xFFE2E3DD) - val md_theme_dark_surface = Color(0xFF1A1C19) - val md_theme_dark_onSurface = Color(0xFFE2E3DD) - val md_theme_dark_surfaceVariant = Color(0xFF424940) - val md_theme_dark_onSurfaceVariant = Color(0xFFC2C9BE) - val md_theme_dark_outline = Color(0xFF8C9389) - val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19) - val md_theme_dark_inverseSurface = Color(0xFFE2E3DD) - val md_theme_dark_inversePrimary = Color(0xFF006E29) - val md_theme_dark_shadow = Color(0xFF000000) - val md_theme_dark_surfaceTint = Color(0xFF79DC84) - val md_theme_dark_outlineVariant = Color(0xFF424940) - val md_theme_dark_scrim = Color(0xFF000000) - - - val seed = Color(0xFF22893C) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/colors/Red.kt b/app/src/main/kotlin/com/meloda/fast/ui/colors/Red.kt deleted file mode 100644 index 4f0aa966..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/colors/Red.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.meloda.fast.ui.colors - -import androidx.compose.ui.graphics.Color - -object Red { - val md_theme_light_primary = Color(0xFFA43A3A) - val md_theme_light_onPrimary = Color(0xFFFFFFFF) - val md_theme_light_primaryContainer = Color(0xFFFFDAD8) - val md_theme_light_onPrimaryContainer = Color(0xFF410006) - val md_theme_light_secondary = Color(0xFF775654) - val md_theme_light_onSecondary = Color(0xFFFFFFFF) - val md_theme_light_secondaryContainer = Color(0xFFFFDAD8) - val md_theme_light_onSecondaryContainer = Color(0xFF2C1514) - val md_theme_light_tertiary = Color(0xFF735A2E) - val md_theme_light_onTertiary = Color(0xFFFFFFFF) - val md_theme_light_tertiaryContainer = Color(0xFFFFDEA9) - val md_theme_light_onTertiaryContainer = Color(0xFF271900) - val md_theme_light_error = Color(0xFFBA1A1A) - val md_theme_light_errorContainer = Color(0xFFFFDAD6) - val md_theme_light_onError = Color(0xFFFFFFFF) - val md_theme_light_onErrorContainer = Color(0xFF410002) - val md_theme_light_background = Color(0xFFFFFBFF) - val md_theme_light_onBackground = Color(0xFF201A1A) - val md_theme_light_surface = Color(0xFFFFFBFF) - val md_theme_light_onSurface = Color(0xFF201A1A) - val md_theme_light_surfaceVariant = Color(0xFFF4DDDC) - val md_theme_light_onSurfaceVariant = Color(0xFF534342) - val md_theme_light_outline = Color(0xFF857372) - val md_theme_light_inverseOnSurface = Color(0xFFFBEEEC) - val md_theme_light_inverseSurface = Color(0xFF362F2E) - val md_theme_light_inversePrimary = Color(0xFFFFB3AF) - val md_theme_light_shadow = Color(0xFF000000) - val md_theme_light_surfaceTint = Color(0xFFA43A3A) - val md_theme_light_outlineVariant = Color(0xFFD7C1C0) - val md_theme_light_scrim = Color(0xFF000000) - - val md_theme_dark_primary = Color(0xFFFFB3AF) - val md_theme_dark_onPrimary = Color(0xFF650912) - val md_theme_dark_primaryContainer = Color(0xFF842225) - val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD8) - val md_theme_dark_secondary = Color(0xFFE7BDBA) - val md_theme_dark_onSecondary = Color(0xFF442928) - val md_theme_dark_secondaryContainer = Color(0xFF5D3F3E) - val md_theme_dark_onSecondaryContainer = Color(0xFFFFDAD8) - val md_theme_dark_tertiary = Color(0xFFE3C28C) - val md_theme_dark_onTertiary = Color(0xFF412D05) - val md_theme_dark_tertiaryContainer = Color(0xFF594319) - val md_theme_dark_onTertiaryContainer = Color(0xFFFFDEA9) - val md_theme_dark_error = Color(0xFFFFB4AB) - val md_theme_dark_errorContainer = Color(0xFF93000A) - val md_theme_dark_onError = Color(0xFF690005) - val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) - val md_theme_dark_background = Color(0xFF201A1A) - val md_theme_dark_onBackground = Color(0xFFEDE0DE) - val md_theme_dark_surface = Color(0xFF201A1A) - val md_theme_dark_onSurface = Color(0xFFEDE0DE) - val md_theme_dark_surfaceVariant = Color(0xFF534342) - val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C0) - val md_theme_dark_outline = Color(0xFFA08C8B) - val md_theme_dark_inverseOnSurface = Color(0xFF201A1A) - val md_theme_dark_inverseSurface = Color(0xFFEDE0DE) - val md_theme_dark_inversePrimary = Color(0xFFA43A3A) - val md_theme_dark_shadow = Color(0xFF000000) - val md_theme_dark_surfaceTint = Color(0xFFFFB3AF) - val md_theme_dark_outlineVariant = Color(0xFF534342) - val md_theme_dark_scrim = Color(0xFF000000) - - - val seed = Color(0xFFC55251) -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/widgets/AsyncImage.kt b/app/src/main/kotlin/com/meloda/fast/ui/widgets/AsyncImage.kt deleted file mode 100644 index 68c5fc89..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/widgets/AsyncImage.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.meloda.fast.ui.widgets - -import androidx.compose.foundation.Image -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalView -import coil.compose.AsyncImage -import coil.request.ImageRequest - - -/** - * Simple wrapper for coil's AsyncImage for showing preview - * @param contentDescription text used by accessibility services to describe what this image - * represents. This should always be provided unless this image is used for decorative purposes, - * and does not represent a meaningful action that a user can take. This text should be - * localized, such as by using [androidx.compose.ui.res.stringResource] or similar - * @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex. - * background) - * @param model Either an [ImageRequest] or the [ImageRequest.data] value. - * @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used - * @param previewPainter Optional painter for preview - */ - -@Composable -fun CoilImage( - contentDescription: String?, - modifier: Modifier, - model: Any?, - contentScale: ContentScale = ContentScale.Fit, - previewPainter: Painter? -) { - if (previewPainter != null && LocalView.current.isInEditMode) { - Image( - painter = previewPainter, - contentDescription = contentDescription, - modifier = modifier - ) - } else { - AsyncImage( - model = model, - contentDescription = contentDescription, - contentScale = contentScale, - modifier = modifier - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt deleted file mode 100644 index db82c31c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.util - -import android.graphics.Color - -object ColorUtils { - - - fun alphaColor(color: Int, alphaFactor: Float): Int { - val alpha = Color.alpha(color) - - val red = Color.red(color) - val green = Color.green(color) - val blue = Color.blue(color) - - return Color.argb((alpha * alphaFactor).toInt(), red, green, blue) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt deleted file mode 100644 index 494655bd..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.meloda.fast.util - -import android.content.Context -import com.meloda.fast.R -import java.text.SimpleDateFormat -import java.util.* -import java.util.concurrent.TimeUnit - -object TimeUtils { - - val OneDayInSeconds get() = TimeUnit.DAYS.toSeconds(1) - - fun removeTime(date: Date): Long { - return Calendar.getInstance().apply { - time = date - this[Calendar.HOUR_OF_DAY] = 0 - this[Calendar.MINUTE] = 0 - this[Calendar.SECOND] = 0 - this[Calendar.MILLISECOND] = 0 - }.timeInMillis - } - - fun getLocalizedDate(context: Context, date: Long): String { - val now = Calendar.getInstance() - val then = Calendar.getInstance().also { it.timeInMillis = date } - - val pattern = when { - now[Calendar.YEAR] != then[Calendar.YEAR] -> "dd MMM yyyy" - now[Calendar.MONTH] != then[Calendar.MONTH] -> "dd MMMM" - now[Calendar.DAY_OF_MONTH] != then[Calendar.DAY_OF_MONTH] -> { - if (now[Calendar.DAY_OF_MONTH] - then[Calendar.DAY_OF_MONTH] == 1) { - return context.getString(R.string.yesterday) - } else { - "dd MMMM" - } - } - else -> return context.getString(R.string.today) - } - - return SimpleDateFormat(pattern, Locale.getDefault()).format(date) - } - - fun getLocalizedTime(context: Context, date: Long): String { - val now = Calendar.getInstance() - val then = Calendar.getInstance().also { it.timeInMillis = date } - - return when { - now[Calendar.YEAR] != then[Calendar.YEAR] -> { - "${now[Calendar.YEAR] - then[Calendar.YEAR]}${ - context.getString(R.string.year_short).lowercase() - }" - } - now[Calendar.MONTH] != then[Calendar.MONTH] -> { - "${now[Calendar.MONTH] - then[Calendar.MONTH]}${ - context.getString(R.string.month_short).lowercase() - }" - } - now[Calendar.DAY_OF_MONTH] != then[Calendar.DAY_OF_MONTH] -> { - val change = now[Calendar.DAY_OF_MONTH] - then[Calendar.DAY_OF_MONTH] - if (change >= 7) { - "${change / 7}${context.getString(R.string.week_short).lowercase()}" - } else { - "$change${context.getString(R.string.day_short).lowercase()}" - } - } - else -> { - if (now[Calendar.MINUTE] == then[Calendar.MINUTE]) { - context.getString(R.string.time_now).lowercase() - } else { - SimpleDateFormat("HH:mm", Locale.getDefault()).format(date) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/view/BoundedLinearLayout.kt b/app/src/main/kotlin/com/meloda/fast/view/BoundedLinearLayout.kt deleted file mode 100644 index 9db7d758..00000000 --- a/app/src/main/kotlin/com/meloda/fast/view/BoundedLinearLayout.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.meloda.fast.view - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.core.content.withStyledAttributes -import com.meloda.fast.R - -@SuppressLint("CustomViewStyleable") -class BoundedLinearLayout : LinearLayoutCompat { - private var mBoundedWidth: Int = 0 - private var mBoundedHeight: Int = 0 - - constructor(context: Context) : super(context) { - mBoundedWidth = 0 - mBoundedHeight = 0 - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - context.withStyledAttributes(attrs, R.styleable.BoundedView) { - mBoundedWidth = getDimensionPixelSize(R.styleable.BoundedView_bounded_width, 0) - mBoundedHeight = getDimensionPixelSize(R.styleable.BoundedView_bounded_height, 0) - } - } - - var maxWidth: Int - get() = mBoundedWidth - set(width) { - if (mBoundedWidth != width) { - mBoundedWidth = width - requestLayout() - } - } - - var maxHeight: Int - get() = mBoundedHeight - set(height) { - if (mBoundedHeight != height) { - mBoundedHeight = height - requestLayout() - } - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - var newWidthMeasureSpec = widthMeasureSpec - var newHeightMeasureSpec = heightMeasureSpec - - val measuredWidth = MeasureSpec.getSize(newWidthMeasureSpec) - if (mBoundedWidth in 1 until measuredWidth) { - val measureMode = MeasureSpec.getMode(newWidthMeasureSpec) - newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mBoundedWidth, measureMode) - } - - val measuredHeight = MeasureSpec.getSize(newHeightMeasureSpec) - if (mBoundedHeight in 1 until measuredHeight) { - val measureMode = MeasureSpec.getMode(newHeightMeasureSpec) - newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mBoundedHeight, measureMode) - } - super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/view/CircleImageView.kt b/app/src/main/kotlin/com/meloda/fast/view/CircleImageView.kt deleted file mode 100644 index 6e574c3f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/view/CircleImageView.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.meloda.fast.view - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Path -import android.graphics.RectF -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatImageView -import androidx.core.view.doOnPreDraw - -class CircleImageView : AppCompatImageView { - - companion object { - val SCALE_TYPE = ScaleType.CENTER_CROP - } - - private var path: Path? = null - private var rect: RectF? = null - - constructor(context: Context) : this(context, null) - - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init() - } - - - override fun onDraw(canvas: Canvas) { - rect?.let { rect -> - if (rect.right == 0F || rect.bottom == 0F) { - createRect(width, height) - } - } - - path?.run { canvas.clipPath(this) } - super.onDraw(canvas) - } - - private fun init() { - scaleType = SCALE_TYPE - - doOnPreDraw { createRect(width, height) } - } - - private fun createRect(width: Int, height: Int) { - path = Path() - rect = RectF(0f, 0f, width.toFloat(), height.toFloat()).apply { - path?.addRoundRect( - this, - width.toFloat() / 2F, - height.toFloat() / 2F, - Path.Direction.CW - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/view/DialogToolbar.kt b/app/src/main/kotlin/com/meloda/fast/view/DialogToolbar.kt deleted file mode 100644 index 0508a9f9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/view/DialogToolbar.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.meloda.fast.view - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.content.withStyledAttributes -import androidx.core.view.updatePaddingRelative -import com.meloda.fast.R -import com.meloda.fast.databinding.ViewDialogToolbarBinding -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.toggleVisibilityIfHasContent -import com.meloda.fast.util.ColorUtils -import kotlin.properties.Delegates - -class DialogToolbar @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { - - private val binding = - ViewDialogToolbarBinding.inflate(LayoutInflater.from(context), this) - - - var title: String? by Delegates.observable(null) { _, _, _ -> - applyTitle(title) - } - - var subtitle: String? by Delegates.observable(null) { _, _, _ -> - applySubtitle(subtitle) - } - - var avatarDrawable: Drawable? by Delegates.observable(null) { _, _, _ -> - applyAvatarDrawable(avatarDrawable) - } - - var avatarClickAction: ((avatar: View) -> Unit)? by Delegates.observable(null) { _, _, _ -> - applyAvatarClickAction(avatarClickAction) - } - - var startIconDrawable: Drawable? by Delegates.observable(null) { _, _, _ -> - applyStartIconDrawable(startIconDrawable) - } - - var startButtonClickAction: (() -> Unit)? = null - - private val defaultBackgroundColor = ContextCompat.getColor( - context, - R.color.colorBackground - ) - - init { - isSaveEnabled = false - - val padding = 4.dpToPx() - updatePaddingRelative(top = padding, bottom = padding) - - context.withStyledAttributes(attrs, R.styleable.DialogToolbar) { - title = getText(R.styleable.DialogToolbar_title)?.toString() - subtitle = getText(R.styleable.DialogToolbar_subtitle)?.toString() - avatarDrawable = getDrawable(R.styleable.DialogToolbar_avatar) - startIconDrawable = getDrawable(R.styleable.DialogToolbar_startIcon) - - val attrBackgroundColor = - getColor(R.styleable.DialogToolbar_backgroundColor, defaultBackgroundColor) - - val useTranslucentBackgroundColor = - getBoolean(R.styleable.DialogToolbar_useTranslucentBackgroundColor, false) - - val backgroundColor = - if (useTranslucentBackgroundColor) ColorUtils.alphaColor(attrBackgroundColor, 0.9F) - else attrBackgroundColor - - setBackgroundColor(backgroundColor) - } - - binding.startIconContainer.setOnClickListener { startButtonClickAction?.invoke() } - } - - private fun syncView() { - applyTitle(title) - applySubtitle(subtitle) - applyAvatarDrawable(avatarDrawable) - applyStartIconDrawable(startIconDrawable) - } - - private fun applyTitle(title: String?) { - binding.title.text = title - binding.title.toggleVisibilityIfHasContent() - } - - private fun applySubtitle(subtitle: String?) { - binding.subtitle.text = subtitle - binding.subtitle.toggleVisibilityIfHasContent() - } - - private fun applyAvatarDrawable(drawable: Drawable?) { - binding.avatar.setImageDrawable(drawable) - binding.avatar.toggleVisibilityIfHasContent() - } - - private fun applyAvatarClickAction(action: ((avatar: View) -> Unit)?) { - binding.avatar.setOnClickListener(action) - } - - private fun applyStartIconDrawable(drawable: Drawable?) { - binding.startIcon.setImageDrawable(drawable) - - binding.startIconContainer.toggleVisibility(drawable != null) - } - - val avatarImageView get() = binding.avatar - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/view/SpaceItemDecoration.kt b/app/src/main/kotlin/com/meloda/fast/view/SpaceItemDecoration.kt deleted file mode 100644 index 874a7a24..00000000 --- a/app/src/main/kotlin/com/meloda/fast/view/SpaceItemDecoration.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.view - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -class SpaceItemDecoration( - private val topMargin: Int? = null, - private val endMargin: Int? = null, - private val bottomMargin: Int? = null, - private val startMargin: Int? = null -) : RecyclerView.ItemDecoration() { - - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - topMargin?.run { outRect.top = this } - endMargin?.run { outRect.right = this } - bottomMargin?.run { outRect.bottom = this } - startMargin?.run { outRect.left = this } - } - -} \ No newline at end of file diff --git a/app/src/main/res/anim/activity_close_enter.xml b/app/src/main/res/anim/activity_close_enter.xml deleted file mode 100644 index 2651d5c9..00000000 --- a/app/src/main/res/anim/activity_close_enter.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/activity_close_exit.xml b/app/src/main/res/anim/activity_close_exit.xml deleted file mode 100644 index a16e9e5f..00000000 --- a/app/src/main/res/anim/activity_close_exit.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/anim/activity_open_enter.xml b/app/src/main/res/anim/activity_open_enter.xml deleted file mode 100644 index 46e1a064..00000000 --- a/app/src/main/res/anim/activity_open_enter.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/anim/activity_open_exit.xml b/app/src/main/res/anim/activity_open_exit.xml deleted file mode 100644 index faf8690c..00000000 --- a/app/src/main/res/anim/activity_open_exit.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/fast_out_extra_slow_in.xml b/app/src/main/res/anim/fast_out_extra_slow_in.xml deleted file mode 100644 index 241111b6..00000000 --- a/app/src/main/res/anim/fast_out_extra_slow_in.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/app/src/main/res/drawable-hdpi/ic_notification_new_message.png b/app/src/main/res/drawable-hdpi/ic_notification_new_message.png deleted file mode 100644 index 61433bfc..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_notification_new_message.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_new_message.png b/app/src/main/res/drawable-mdpi/ic_notification_new_message.png deleted file mode 100644 index aa174d47..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_notification_new_message.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_new_message.png b/app/src/main/res/drawable-xhdpi/ic_notification_new_message.png deleted file mode 100644 index 760af05f..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_notification_new_message.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png b/app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png deleted file mode 100644 index 6499dce6..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml deleted file mode 100644 index 07ad6413..00000000 --- a/app/src/main/res/drawable/ic_back.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_account_circle_24.xml b/app/src/main/res/drawable/ic_baseline_account_circle_24.xml deleted file mode 100644 index 1fc37dc3..00000000 --- a/app/src/main/res/drawable/ic_baseline_account_circle_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml b/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml deleted file mode 100644 index 4e6ca60c..00000000 --- a/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_close_in_circle.xml b/app/src/main/res/drawable/ic_close_in_circle.xml deleted file mode 100644 index 96c95cab..00000000 --- a/app/src/main/res/drawable/ic_close_in_circle.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_image_button_circle_background.xml b/app/src/main/res/drawable/ic_image_button_circle_background.xml deleted file mode 100644 index b50a43d5..00000000 --- a/app/src/main/res/drawable/ic_image_button_circle_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_key.xml b/app/src/main/res/drawable/ic_key.xml deleted file mode 100644 index 581f83e0..00000000 --- a/app/src/main/res/drawable/ic_key.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml b/app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml deleted file mode 100644 index 35af533c..00000000 --- a/app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_in_background.xml b/app/src/main/res/drawable/ic_message_in_background.xml deleted file mode 100644 index e2aa9732..00000000 --- a/app/src/main/res/drawable/ic_message_in_background.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_in_background_middle.xml b/app/src/main/res/drawable/ic_message_in_background_middle.xml deleted file mode 100644 index 2000155e..00000000 --- a/app/src/main/res/drawable/ic_message_in_background_middle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background.xml b/app/src/main/res/drawable/ic_message_out_background.xml deleted file mode 100644 index 31e9209f..00000000 --- a/app/src/main/res/drawable/ic_message_out_background.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background_middle.xml b/app/src/main/res/drawable/ic_message_out_background_middle.xml deleted file mode 100644 index d1c61162..00000000 --- a/app/src/main/res/drawable/ic_message_out_background_middle.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml b/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml deleted file mode 100644 index 463bcc87..00000000 --- a/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background_stroke.xml b/app/src/main/res/drawable/ic_message_out_background_stroke.xml deleted file mode 100644 index e16a5641..00000000 --- a/app/src/main/res/drawable/ic_message_out_background_stroke.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_panel_background.xml b/app/src/main/res/drawable/ic_message_panel_background.xml deleted file mode 100644 index ff07f7a5..00000000 --- a/app/src/main/res/drawable/ic_message_panel_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_panel_gradient.xml b/app/src/main/res/drawable/ic_message_panel_gradient.xml deleted file mode 100644 index 0b00bdf8..00000000 --- a/app/src/main/res/drawable/ic_message_panel_gradient.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_unread.xml b/app/src/main/res/drawable/ic_message_unread.xml deleted file mode 100644 index 341a6369..00000000 --- a/app/src/main/res/drawable/ic_message_unread.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_messages_history_toolbar_gradient_background.xml b/app/src/main/res/drawable/ic_messages_history_toolbar_gradient_background.xml deleted file mode 100644 index eafd6329..00000000 --- a/app/src/main/res/drawable/ic_messages_history_toolbar_gradient_background.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notification_new_message.xml b/app/src/main/res/drawable/ic_notification_new_message.xml deleted file mode 100644 index 776ab808..00000000 --- a/app/src/main/res/drawable/ic_notification_new_message.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_online_pc.xml b/app/src/main/res/drawable/ic_online_pc.xml deleted file mode 100644 index 1f7e959a..00000000 --- a/app/src/main/res/drawable/ic_online_pc.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_people_outline.xml b/app/src/main/res/drawable/ic_people_outline.xml deleted file mode 100644 index 2f8cbd63..00000000 --- a/app/src/main/res/drawable/ic_people_outline.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_phantom.xml b/app/src/main/res/drawable/ic_phantom.xml deleted file mode 100644 index 3bc9d504..00000000 --- a/app/src/main/res/drawable/ic_phantom.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_play_button_circle_background.xml b/app/src/main/res/drawable/ic_play_button_circle_background.xml deleted file mode 100644 index 96c95cab..00000000 --- a/app/src/main/res/drawable/ic_play_button_circle_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_round_access_time_24.xml b/app/src/main/res/drawable/ic_round_access_time_24.xml deleted file mode 100644 index b0aaa17c..00000000 --- a/app/src/main/res/drawable/ic_round_access_time_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_close_20.xml b/app/src/main/res/drawable/ic_round_close_20.xml deleted file mode 100644 index 25b4f3e6..00000000 --- a/app/src/main/res/drawable/ic_round_close_20.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_emoji_emotions_24.xml b/app/src/main/res/drawable/ic_round_emoji_emotions_24.xml deleted file mode 100644 index c8e85353..00000000 --- a/app/src/main/res/drawable/ic_round_emoji_emotions_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_error_outline_24.xml b/app/src/main/res/drawable/ic_round_error_outline_24.xml deleted file mode 100644 index 7d643610..00000000 --- a/app/src/main/res/drawable/ic_round_error_outline_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_group_24.xml b/app/src/main/res/drawable/ic_round_group_24.xml deleted file mode 100644 index 7704f2b3..00000000 --- a/app/src/main/res/drawable/ic_round_group_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_keyboard_arrow_down_24.xml b/app/src/main/res/drawable/ic_round_keyboard_arrow_down_24.xml deleted file mode 100644 index a8340c19..00000000 --- a/app/src/main/res/drawable/ic_round_keyboard_arrow_down_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_link_24.xml b/app/src/main/res/drawable/ic_round_link_24.xml deleted file mode 100644 index a7c819ed..00000000 --- a/app/src/main/res/drawable/ic_round_link_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_mail_24.xml b/app/src/main/res/drawable/ic_round_mail_24.xml deleted file mode 100644 index d9a337d0..00000000 --- a/app/src/main/res/drawable/ic_round_mail_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_mic_24.xml b/app/src/main/res/drawable/ic_round_mic_24.xml deleted file mode 100644 index 57219f6e..00000000 --- a/app/src/main/res/drawable/ic_round_mic_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_play_arrow_24.xml b/app/src/main/res/drawable/ic_round_play_arrow_24.xml deleted file mode 100644 index 78fbfbba..00000000 --- a/app/src/main/res/drawable/ic_round_play_arrow_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_send_24.xml b/app/src/main/res/drawable/ic_round_send_24.xml deleted file mode 100644 index ae931b57..00000000 --- a/app/src/main/res/drawable/ic_round_send_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_settings_24.xml b/app/src/main/res/drawable/ic_round_settings_24.xml deleted file mode 100644 index a277a99d..00000000 --- a/app/src/main/res/drawable/ic_round_settings_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_settings_primary.xml b/app/src/main/res/drawable/ic_round_settings_primary.xml deleted file mode 100644 index 1d4f7900..00000000 --- a/app/src/main/res/drawable/ic_round_settings_primary.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_round_star_24.xml b/app/src/main/res/drawable/ic_round_star_24.xml deleted file mode 100644 index f62410ba..00000000 --- a/app/src/main/res/drawable/ic_round_star_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml deleted file mode 100644 index 01045afd..00000000 --- a/app/src/main/res/drawable/ic_search.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_security.xml b/app/src/main/res/drawable/ic_security.xml deleted file mode 100644 index ef2e5521..00000000 --- a/app/src/main/res/drawable/ic_security.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_star_border.xml b/app/src/main/res/drawable/ic_star_border.xml deleted file mode 100644 index f341eb01..00000000 --- a/app/src/main/res/drawable/ic_star_border.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/time_read_indicator_on_attachments_background.xml b/app/src/main/res/drawable/time_read_indicator_on_attachments_background.xml deleted file mode 100644 index cf263555..00000000 --- a/app/src/main/res/drawable/time_read_indicator_on_attachments_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/font/tt_commons_bold.ttf b/app/src/main/res/font/tt_commons_bold.ttf deleted file mode 100644 index 98aa0411..00000000 Binary files a/app/src/main/res/font/tt_commons_bold.ttf and /dev/null differ diff --git a/app/src/main/res/font/tt_commons_medium.ttf b/app/src/main/res/font/tt_commons_medium.ttf deleted file mode 100644 index 730b1fa1..00000000 Binary files a/app/src/main/res/font/tt_commons_medium.ttf and /dev/null differ diff --git a/app/src/main/res/font/tt_commons_regular.ttf b/app/src/main/res/font/tt_commons_regular.ttf deleted file mode 100644 index 545705cf..00000000 Binary files a/app/src/main/res/font/tt_commons_regular.ttf and /dev/null differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 09341829..00000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml deleted file mode 100644 index 0cb45b0a..00000000 --- a/app/src/main/res/layout/dialog_captcha.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_fast_login.xml b/app/src/main/res/layout/dialog_fast_login.xml deleted file mode 100644 index f20d70b0..00000000 --- a/app/src/main/res/layout/dialog_fast_login.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_message_delete.xml b/app/src/main/res/layout/dialog_message_delete.xml deleted file mode 100644 index a021c392..00000000 --- a/app/src/main/res/layout/dialog_message_delete.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_validation.xml b/app/src/main/res/layout/dialog_validation.xml deleted file mode 100644 index 6f636d65..00000000 --- a/app/src/main/res/layout/dialog_validation.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml deleted file mode 100644 index 915e4c3f..00000000 --- a/app/src/main/res/layout/drawer_header.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat_info.xml b/app/src/main/res/layout/fragment_chat_info.xml deleted file mode 100644 index 963d3e44..00000000 --- a/app/src/main/res/layout/fragment_chat_info.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat_info_members.xml b/app/src/main/res/layout/fragment_chat_info_members.xml deleted file mode 100644 index ac3270d3..00000000 --- a/app/src/main/res/layout/fragment_chat_info_members.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_conversations.xml b/app/src/main/res/layout/fragment_conversations.xml deleted file mode 100644 index 1499f534..00000000 --- a/app/src/main/res/layout/fragment_conversations.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_forwarded_messages.xml b/app/src/main/res/layout/fragment_forwarded_messages.xml deleted file mode 100644 index 828374c7..00000000 --- a/app/src/main/res/layout/fragment_forwarded_messages.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_messages_history.xml b/app/src/main/res/layout/fragment_messages_history.xml deleted file mode 100644 index 32435b4d..00000000 --- a/app/src/main/res/layout/fragment_messages_history.xml +++ /dev/null @@ -1,297 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml deleted file mode 100644 index bca1d865..00000000 --- a/app/src/main/res/layout/fragment_settings.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_updates.xml b/app/src/main/res/layout/fragment_updates.xml deleted file mode 100644 index 20cfcb9e..00000000 --- a/app/src/main/res/layout/fragment_updates.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_user_banned.xml b/app/src/main/res/layout/fragment_user_banned.xml deleted file mode 100644 index c0b90365..00000000 --- a/app/src/main/res/layout/fragment_user_banned.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_chat_member.xml b/app/src/main/res/layout/item_chat_member.xml deleted file mode 100644 index 4b6d30ff..00000000 --- a/app/src/main/res/layout/item_chat_member.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_conversation.xml b/app/src/main/res/layout/item_conversation.xml deleted file mode 100644 index 00c71acf..00000000 --- a/app/src/main/res/layout/item_conversation.xml +++ /dev/null @@ -1,256 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_audio.xml b/app/src/main/res/layout/item_message_attachment_audio.xml deleted file mode 100644 index eccc8c89..00000000 --- a/app/src/main/res/layout/item_message_attachment_audio.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_message_attachment_call.xml b/app/src/main/res/layout/item_message_attachment_call.xml deleted file mode 100644 index 750f452f..00000000 --- a/app/src/main/res/layout/item_message_attachment_call.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_file.xml b/app/src/main/res/layout/item_message_attachment_file.xml deleted file mode 100644 index 0d83ba32..00000000 --- a/app/src/main/res/layout/item_message_attachment_file.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_forwards.xml b/app/src/main/res/layout/item_message_attachment_forwards.xml deleted file mode 100644 index bb0f2f9a..00000000 --- a/app/src/main/res/layout/item_message_attachment_forwards.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_geo.xml b/app/src/main/res/layout/item_message_attachment_geo.xml deleted file mode 100644 index 950b9f77..00000000 --- a/app/src/main/res/layout/item_message_attachment_geo.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_gift.xml b/app/src/main/res/layout/item_message_attachment_gift.xml deleted file mode 100644 index 8b89d013..00000000 --- a/app/src/main/res/layout/item_message_attachment_gift.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_graffiti.xml b/app/src/main/res/layout/item_message_attachment_graffiti.xml deleted file mode 100644 index 8b89d013..00000000 --- a/app/src/main/res/layout/item_message_attachment_graffiti.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_link.xml b/app/src/main/res/layout/item_message_attachment_link.xml deleted file mode 100644 index 584e5008..00000000 --- a/app/src/main/res/layout/item_message_attachment_link.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_photo.xml b/app/src/main/res/layout/item_message_attachment_photo.xml deleted file mode 100644 index 97f91bbd..00000000 --- a/app/src/main/res/layout/item_message_attachment_photo.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_reply.xml b/app/src/main/res/layout/item_message_attachment_reply.xml deleted file mode 100644 index 0181df75..00000000 --- a/app/src/main/res/layout/item_message_attachment_reply.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_sticker.xml b/app/src/main/res/layout/item_message_attachment_sticker.xml deleted file mode 100644 index 56adcaca..00000000 --- a/app/src/main/res/layout/item_message_attachment_sticker.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_story.xml b/app/src/main/res/layout/item_message_attachment_story.xml deleted file mode 100644 index 7698f47e..00000000 --- a/app/src/main/res/layout/item_message_attachment_story.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_video.xml b/app/src/main/res/layout/item_message_attachment_video.xml deleted file mode 100644 index 1829dd48..00000000 --- a/app/src/main/res/layout/item_message_attachment_video.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_voice.xml b/app/src/main/res/layout/item_message_attachment_voice.xml deleted file mode 100644 index 4532d531..00000000 --- a/app/src/main/res/layout/item_message_attachment_voice.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_wall_post.xml b/app/src/main/res/layout/item_message_attachment_wall_post.xml deleted file mode 100644 index a4e0a877..00000000 --- a/app/src/main/res/layout/item_message_attachment_wall_post.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_in.xml b/app/src/main/res/layout/item_message_in.xml deleted file mode 100644 index f955d8d9..00000000 --- a/app/src/main/res/layout/item_message_in.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_out.xml b/app/src/main/res/layout/item_message_out.xml deleted file mode 100644 index 95c03742..00000000 --- a/app/src/main/res/layout/item_message_out.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_service.xml b/app/src/main/res/layout/item_message_service.xml deleted file mode 100644 index dc0dd047..00000000 --- a/app/src/main/res/layout/item_message_service.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_checkbox.xml b/app/src/main/res/layout/item_settings_checkbox.xml deleted file mode 100644 index d3959648..00000000 --- a/app/src/main/res/layout/item_settings_checkbox.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_edit_text.xml b/app/src/main/res/layout/item_settings_edit_text.xml deleted file mode 100644 index 06331356..00000000 --- a/app/src/main/res/layout/item_settings_edit_text.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_edit_text_alert.xml b/app/src/main/res/layout/item_settings_edit_text_alert.xml deleted file mode 100644 index 6a23eae5..00000000 --- a/app/src/main/res/layout/item_settings_edit_text_alert.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/item_settings_list.xml b/app/src/main/res/layout/item_settings_list.xml deleted file mode 100644 index 9106ecae..00000000 --- a/app/src/main/res/layout/item_settings_list.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/item_settings_switch.xml b/app/src/main/res/layout/item_settings_switch.xml deleted file mode 100644 index 2328e3db..00000000 --- a/app/src/main/res/layout/item_settings_switch.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/item_settings_title.xml b/app/src/main/res/layout/item_settings_title.xml deleted file mode 100644 index 54a00174..00000000 --- a/app/src/main/res/layout/item_settings_title.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_title_summary.xml b/app/src/main/res/layout/item_settings_title_summary.xml deleted file mode 100644 index 06331356..00000000 --- a/app/src/main/res/layout/item_settings_title_summary.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_uploaded_attachment_audio.xml b/app/src/main/res/layout/item_uploaded_attachment_audio.xml deleted file mode 100644 index a85ffdca..00000000 --- a/app/src/main/res/layout/item_uploaded_attachment_audio.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_uploaded_attachment_file.xml b/app/src/main/res/layout/item_uploaded_attachment_file.xml deleted file mode 100644 index b5fd2bab..00000000 --- a/app/src/main/res/layout/item_uploaded_attachment_file.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_uploaded_attachment_photo.xml b/app/src/main/res/layout/item_uploaded_attachment_photo.xml deleted file mode 100644 index 1e02c66e..00000000 --- a/app/src/main/res/layout/item_uploaded_attachment_photo.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_uploaded_attachment_video.xml b/app/src/main/res/layout/item_uploaded_attachment_video.xml deleted file mode 100644 index 6a36733e..00000000 --- a/app/src/main/res/layout/item_uploaded_attachment_video.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_menu_item_avatar.xml b/app/src/main/res/layout/toolbar_menu_item_avatar.xml deleted file mode 100644 index 68ca49af..00000000 --- a/app/src/main/res/layout/toolbar_menu_item_avatar.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_dialog_toolbar.xml b/app/src/main/res/layout/view_dialog_toolbar.xml deleted file mode 100644 index a99cca08..00000000 --- a/app/src/main/res/layout/view_dialog_toolbar.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_bottom.xml b/app/src/main/res/menu/activity_main_bottom.xml deleted file mode 100644 index 36b364d7..00000000 --- a/app/src/main/res/menu/activity_main_bottom.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml deleted file mode 100644 index 3da21556..00000000 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_conversations.xml b/app/src/main/res/menu/fragment_conversations.xml deleted file mode 100644 index 167638f2..00000000 --- a/app/src/main/res/menu/fragment_conversations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/menu/fragment_conversations_popup.xml b/app/src/main/res/menu/fragment_conversations_popup.xml deleted file mode 100644 index b05248ac..00000000 --- a/app/src/main/res/menu/fragment_conversations_popup.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/values-night/bools.xml b/app/src/main/res/values-night/bools.xml deleted file mode 100644 index d1af27f2..00000000 --- a/app/src/main/res/values-night/bools.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - false - false - - \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml deleted file mode 100644 index 6f14ead9..00000000 --- a/app/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - #40000000 - diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml deleted file mode 100644 index d102c852..00000000 --- a/app/src/main/res/values-ru/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Вложения - Настроечбки - Настроечбки - diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml deleted file mode 100644 index 6646f962..00000000 --- a/app/src/main/res/values-v27/themes.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml deleted file mode 100644 index 9516afa1..00000000 --- a/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index d10e89d9..c37be342 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -2,4 +2,5 @@ + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..ed8839d4 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml deleted file mode 100644 index d8cf32d5..00000000 --- a/app/src/main/res/xml/preferences.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml deleted file mode 100644 index 381211ff..00000000 --- a/app/src/main/res/xml/shortcuts.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/app/src/staging/res/mipmap-hdpi/ic_launcher.png b/app/src/staging/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..198f18ea Binary files /dev/null and b/app/src/staging/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/staging/res/mipmap-mdpi/ic_launcher.png b/app/src/staging/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..12b7c714 Binary files /dev/null and b/app/src/staging/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/staging/res/mipmap-xhdpi/ic_launcher.png b/app/src/staging/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..37a31bb6 Binary files /dev/null and b/app/src/staging/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/staging/res/mipmap-xxhdpi/ic_launcher.png b/app/src/staging/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..4194db6f Binary files /dev/null and b/app/src/staging/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/staging/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/staging/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..ee939bd8 Binary files /dev/null and b/app/src/staging/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/dev/res/values/ic_launcher_background.xml b/app/src/staging/res/values/ic_launcher_background.xml similarity index 100% rename from app/src/dev/res/values/ic_launcher_background.xml rename to app/src/staging/res/values/ic_launcher_background.xml diff --git a/app/src/staging/res/values/strings.xml b/app/src/staging/res/values/strings.xml new file mode 100644 index 00000000..aef1013b --- /dev/null +++ b/app/src/staging/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Fast Staging + diff --git a/build.gradle.kts b/build.gradle.kts index 58a69141..c8cdbe3b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,28 +1,28 @@ -buildscript { - - repositories { - google() - mavenCentral() - } - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") - classpath("com.android.tools.build:gradle:8.1.0") - } -} +import dev.iurysouza.modulegraph.Theme plugins { - id("org.jetbrains.kotlin.android") version "1.8.20" apply false - id("com.google.devtools.ksp") version "1.8.20-1.0.11" apply false + alias(libs.plugins.com.android.application) apply false + alias(libs.plugins.org.jetbrains.kotlin.android) apply false + alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize) apply false + alias(libs.plugins.android.library) apply false + + id("dev.iurysouza.modulegraph") version "0.8.1" } -allprojects { - repositories { - google() - mavenCentral() - maven(url = "https://jitpack.io") - } -} - -tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) +moduleGraphConfig { + readmePath.set("${rootDir}/README.md") + heading.set("### Module Graph") + theme.set( + Theme.BASE( + mapOf( + "primaryTextColor" to "#fff", + "primaryColor" to "#5a4f7c", + "primaryBorderColor" to "#5a4f7c", + "lineColor" to "#f5a623", + "tertiaryColor" to "#40375c", + "fontSize" to "12px", + ), + focusColor = "#FA8140" + ), + ) } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..b6413e30 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..29744ec1 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "buildSrc" diff --git a/buildSrc/src/main/kotlin/Configs.kt b/buildSrc/src/main/kotlin/Configs.kt new file mode 100644 index 00000000..ca8e7c9a --- /dev/null +++ b/buildSrc/src/main/kotlin/Configs.kt @@ -0,0 +1,13 @@ +import org.gradle.api.JavaVersion + +object Configs { + + const val appCode = 1 + const val appName = "1.8.1" + + const val compileSdk = 34 + const val minSdk = 24 + const val targetSdk = 34 + + val java = JavaVersion.VERSION_17 +} diff --git a/core/common/.gitignore b/core/common/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts new file mode 100644 index 00000000..394009eb --- /dev/null +++ b/core/common/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize) + alias(libs.plugins.kotlin.compose.compiler) +} + +group = "com.meloda.app.fast.common" + +android { + namespace = "com.meloda.app.fast.common" + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers") + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.core.ktx) + implementation(libs.preference.ktx) + + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.runtime.ktx) + + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.androidx.compose.navigation) + + implementation(libs.coil.compose) + + implementation(libs.nanokt.jvm) + implementation(libs.nanokt.android) + implementation(libs.nanokt) + + implementation(libs.androidx.navigation.compose) + + implementation(libs.kotlin.serialization) +} diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/common/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt new file mode 100644 index 00000000..225c576e --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt @@ -0,0 +1,10 @@ +package com.meloda.app.fast.common + +object AppConstants { + + const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive" + + const val API_VERSION = "5.173" + const val URL_OAUTH = "https://oauth.vk.com" + const val URL_API = "https://api.vk.com/method" +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/AuthInterceptor.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/AuthInterceptor.kt new file mode 100644 index 00000000..74a61c10 --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/AuthInterceptor.kt @@ -0,0 +1,31 @@ +package com.meloda.app.fast.common + +import androidx.core.net.toUri +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 uri = builder.build().toUri().toString().toUri() + + if (uri.getQueryParameter("v") == null) { + builder.addQueryParameter( + name = "v", + value = URLEncoder.encode(AppConstants.API_VERSION, "utf-8") + ) + } + + if (UserConfig.accessToken.isNotBlank()) { + builder.addQueryParameter( + "access_token", + URLEncoder.encode(UserConfig.accessToken, "utf-8") + ) + } + + return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build()) + } +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/CustomNavType.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/CustomNavType.kt new file mode 100644 index 00000000..23d7af9a --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/CustomNavType.kt @@ -0,0 +1,22 @@ +package com.meloda.app.fast.common + +import android.os.Bundle +import android.os.Parcelable +import androidx.core.os.BundleCompat +import androidx.navigation.NavType +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +inline fun customNavType( + isNullableAllowed: Boolean = false, + json: Json = Json +) = object : NavType(isNullableAllowed = isNullableAllowed) { + override fun get(bundle: Bundle, key: String) = + BundleCompat.getParcelable(bundle, key, T::class.java) + + override fun parseValue(value: String): T = json.decodeFromString(value) + + override fun serializeAsValue(value: T): String = json.encodeToString(value) + + override fun put(bundle: Bundle, key: String, value: T) = bundle.putParcelable(key, value) +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/UiImage.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/UiImage.kt new file mode 100644 index 00000000..709f50cd --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/UiImage.kt @@ -0,0 +1,29 @@ +package com.meloda.app.fast.common + +import android.graphics.drawable.Drawable +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes + +sealed class UiImage { + + data class Resource(@DrawableRes val resId: Int) : UiImage() + + data class Simple(val drawable: Drawable) : UiImage() + + data class Color(@ColorInt val color: Int) : UiImage() + + data class ColorResource(@ColorRes val resId: Int) : UiImage() + + data class Url(val url: String) : UiImage() + + fun extractUrl(): String? = when (this) { + is Url -> this.url + else -> null + } + + fun extractResId(): Int = when (this) { + is Resource -> this.resId + else -> throw IllegalStateException("this UiImage is not Resource") + } +} diff --git a/app/src/main/kotlin/com/meloda/fast/model/base/UiText.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/UiText.kt similarity index 52% rename from app/src/main/kotlin/com/meloda/fast/model/base/UiText.kt rename to core/common/src/main/kotlin/com/meloda/app/fast/common/UiText.kt index 6fdcac24..0f06da12 100644 --- a/app/src/main/kotlin/com/meloda/fast/model/base/UiText.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/UiText.kt @@ -1,20 +1,18 @@ -package com.meloda.fast.model.base +package com.meloda.app.fast.common -import android.content.Context -import android.os.Parcelable +import android.content.res.Resources import androidx.annotation.PluralsRes import androidx.annotation.StringRes -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue -@Parcelize -sealed class UiText : Parcelable { +sealed class UiText { + + data object Empty : UiText() data class Resource(@StringRes val resId: Int) : UiText() data class ResourceParams( @StringRes val value: Int, - val args: List<@RawValue Any?>, + val args: List, ) : UiText() data class Simple(val text: String) : UiText() @@ -22,20 +20,20 @@ sealed class UiText : Parcelable { data class QuantityResource(@PluralsRes val resId: Int, val quantity: Int) : UiText() } -fun UiText?.parseString(context: Context): String? { +fun UiText?.parseString(resources: Resources): String? { return when (this) { - is UiText.Resource -> context.getString(resId) + is UiText.Resource -> resources.getString(resId) is UiText.ResourceParams -> { val processedArgs = args.map { any -> when (any) { - is UiText -> any.parseString(context) + is UiText -> any.parseString(resources) else -> any } } - context.getString(value, *processedArgs.toTypedArray()) + resources.getString(value, *processedArgs.toTypedArray()) } - is UiText.QuantityResource -> context.resources.getQuantityString(resId, quantity, quantity) + is UiText.QuantityResource -> resources.getQuantityString(resId, quantity, quantity) is UiText.Simple -> text else -> null } diff --git a/app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/UserConfig.kt similarity index 56% rename from app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt rename to core/common/src/main/kotlin/com/meloda/app/fast/common/UserConfig.kt index e0d69f7c..490a615b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/UserConfig.kt @@ -1,18 +1,18 @@ -package com.meloda.fast.api +package com.meloda.app.fast.common +import android.content.SharedPreferences import androidx.core.content.edit -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.model.AppAccount -import kotlinx.coroutines.flow.MutableStateFlow +import kotlin.properties.Delegates object UserConfig { private const val ARG_CURRENT_USER_ID = "current_user_id" - const val FAST_APP_ID = "6964679" + private var preferences: SharedPreferences by Delegates.notNull() - private val preferences get() = AppGlobal.preferences + fun init(preferences: SharedPreferences) { + this.preferences = preferences + } var currentUserId: Int = -1 get() = preferences.getInt(ARG_CURRENT_USER_ID, -1) @@ -24,12 +24,7 @@ object UserConfig { var userId: Int = -1 var accessToken: String = "" var fastToken: String? = "" - - fun parse(account: AppAccount) { - this.userId = account.userId - this.accessToken = account.accessToken - this.fastToken = account.fastToken - } + var trustedHash: String? = null fun clear() { currentUserId = -1 @@ -41,7 +36,4 @@ object UserConfig { fun isLoggedIn(): Boolean { return currentUserId > 0 && userId > 0 && accessToken.isNotBlank() } - - val vkUser: MutableStateFlow = MutableStateFlow(null) - } diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/VkConstants.kt similarity index 61% rename from app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt rename to core/common/src/main/kotlin/com/meloda/app/fast/common/VkConstants.kt index e675fe2b..d126da24 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/VkConstants.kt @@ -1,18 +1,15 @@ -package com.meloda.fast.api +package com.meloda.app.fast.common -import com.meloda.fast.api.model.attachments.* - -@Suppress("RemoveExplicitTypeArguments") -object VKConstants { +object VkConstants { const val GROUP_FIELDS = "description,members_count,counters,status,verified" const val USER_FIELDS = "photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info,bdate" - const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS" + const val ALL_FIELDS = + "$USER_FIELDS,$GROUP_FIELDS" - const val API_VERSION = "5.173" const val LP_VERSION = 10 const val VK_APP_ID = "2274003" @@ -42,15 +39,15 @@ object VKConstants { } } - val restrictedToEditAttachments = listOf>( - VkCall::class.java, - VkCurator::class.java, - VkEvent::class.java, - VkGift::class.java, - VkGraffiti::class.java, - VkGroupCall::class.java, - VkStory::class.java, - VkVoiceMessage::class.java, - VkWidget::class.java - ) +// val restrictedToEditAttachments = listOf>( +// VkCallDomain::class.java, +// VkCuratorDomain::class.java, +// VkEventDomain::class.java, +// VkGiftDomain::class.java, +// VkGraffitiDomain::class.java, +// VkGroupCallDomain::class.java, +// VkStoryDomain::class.java, +// VkAudioMessageDomain::class.java, +// VkWidgetDomain::class.java +// ) } diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/di/CommonModule.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/di/CommonModule.kt new file mode 100644 index 00000000..633eaa09 --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/di/CommonModule.kt @@ -0,0 +1,12 @@ +package com.meloda.app.fast.common.di + +import coil.ImageLoader +import org.koin.dsl.module + +val commonModule = module { + single { + ImageLoader.Builder(get()) + .crossfade(true) + .build() + } +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt new file mode 100644 index 00000000..0b8b2a8a --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt @@ -0,0 +1,151 @@ +package com.meloda.app.fast.common.extensions + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +fun Context.restartApp() { + (this as? Activity)?.let { activity -> + activity.finishAffinity() + activity.startActivity( + Intent( + this, + Class.forName("com.meloda.app.fast.MainActivity") + ) + ) + } +} + +inline fun Iterable.findWithIndex(predicate: (T) -> Boolean): Pair? { + val value = firstOrNull(predicate) ?: return null + return indexOf(value).let { index -> if (index == -1) null else index to value } +} + +fun MutableList.addIf(element: T, condition: () -> Boolean) { + if (condition.invoke()) add(element) +} + +context(ViewModel) +fun Flow.listenValue(action: suspend (T) -> Unit) = listenValue(viewModelScope, action) + +fun Flow.listenValue( + coroutineScope: CoroutineScope, + action: suspend (T) -> Unit +): Job = onEach(action::invoke).launchIn(coroutineScope) + +fun createTimerFlow( + time: Int, + onStartAction: (suspend () -> Unit)? = null, + onTickAction: (suspend (remainedTime: Int) -> Unit)? = null, + onTimeoutAction: (suspend () -> Unit)? = null, + interval: Duration = 1.seconds +): Flow = (time downTo 0) + .asSequence() + .asFlow() + .onStart { onStartAction?.invoke() } + .onEach { timeLeft -> + onTickAction?.invoke(timeLeft) + if (timeLeft == 0) { + onTimeoutAction?.invoke() + } else { + delay(interval) + } + } + +fun createTimerFlow( + isNeedToEndCondition: suspend () -> Boolean, + onStartAction: (suspend () -> Unit)? = null, + onTickAction: (suspend () -> Unit)? = null, + onEndAction: (suspend () -> Unit)? = null, + interval: Duration = 1.seconds +): Flow = flow { + while (true) { + val isNeedToEnd = isNeedToEndCondition() + emit(isNeedToEnd) + if (isNeedToEnd) break + } +} + .onStart { onStartAction?.invoke() } + .onEach { isNeedToEnd -> + onTickAction?.invoke() + if (isNeedToEnd) { + onEndAction?.invoke() + } else { + delay(interval) + } + } + +context(ViewModel) +fun MutableSharedFlow.emitOnMainScope(value: T) = emitOnScope(Dispatchers.Main) { value } + +context(ViewModel) +fun MutableSharedFlow.emitOnScope( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + value: () -> T, +) { + viewModelScope.launch(coroutineContext) { + emit(value()) + } +} + +context(CoroutineScope) +suspend fun MutableSharedFlow.emitWithMain(value: T) { + withContext(Dispatchers.Main) { + emit(value) + } +} + +context(ViewModel) +fun MutableStateFlow.updateValue(newValue: T) = this.update { newValue } + +fun MutableStateFlow.setValue(function: (T) -> T) { + val newValue = function(value) + update { newValue } +} + +fun Any.asInt(): Int { + return when (this) { + is Number -> this.toInt() + + else -> throw IllegalArgumentException("Object is not numeric") + } +} + +fun Any.toList(mapper: (old: Any) -> T): List { + return when (this) { + is List<*> -> this.mapNotNull { it?.run(mapper) } + + else -> emptyList() + } +} + +fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean { + return if (Build.VERSION.SDK_INT >= sdkInt) { + action?.invoke() + true + } else { + false + } +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/StringsExtensions.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/StringsExtensions.kt new file mode 100644 index 00000000..e547bc87 --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/StringsExtensions.kt @@ -0,0 +1,17 @@ +package com.meloda.app.fast.common.extensions + +inline fun String?.ifEmpty(defaultValue: () -> String?): String? = + if (this?.isEmpty() == true) defaultValue() else this + +fun String?.orDots(count: Int = 3): String { + return this ?: ("." * count) +} + +operator fun String.times(count: Int): String { + val builder = StringBuilder() + for (i in 0 until count) { + builder.append(this) + } + + return builder.toString() +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/navigation/NavigationExtensions.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/navigation/NavigationExtensions.kt new file mode 100644 index 00000000..c5a7ad42 --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/navigation/NavigationExtensions.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.common.extensions.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import org.koin.androidx.compose.koinViewModel +import org.koin.androidx.compose.navigation.koinNavViewModel + +@Composable +inline fun NavBackStackEntry.sharedViewModel(navController: NavController): T { + val navGraphRoute = destination.parent?.route ?: return koinViewModel() + val parentEntry = remember(this) { + navController.getBackStackEntry(navGraphRoute) + } + return koinNavViewModel(viewModelStoreOwner = parentEntry) +} diff --git a/app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt similarity index 72% rename from app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt rename to core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt index 83c34eee..43c11b0c 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt @@ -1,82 +1,58 @@ -package com.meloda.fast.util +package com.meloda.app.fast.common.util import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.content.res.Resources import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.PowerManager import android.provider.Settings -import android.util.TypedValue import android.widget.Toast -import androidx.annotation.AttrRes import androidx.core.content.FileProvider -import androidx.core.graphics.Insets -import androidx.core.view.WindowInsetsCompat -import com.meloda.fast.BuildConfig -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.isTrue import java.io.File import java.io.FileOutputStream +private object BuildConfig { + const val DEBUG = true + const val APPLICATION_ID = "com.meloda.app.fast" +} object AndroidUtils { - fun getDisplayWidth(): Int { - return Resources.getSystem().displayMetrics.widthPixels - } - - fun getDisplayHeight(): Int { - return Resources.getSystem().displayMetrics.heightPixels - } - fun copyText( + context: Context, label: String? = "", text: String, withToast: Boolean = false ) { val clipboardManager = - AppGlobal.Instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text)) if (withToast && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(AppGlobal.Instance, "Copied to clipboard", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() } } fun copyImage( + context: Context, label: String? = "", imageUri: Uri, withToast: Boolean = false ) { val clipboardManager = - AppGlobal.Instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager.setPrimaryClip(ClipData.newRawUri(label, imageUri)) if (withToast && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(AppGlobal.Instance, "Copied to clipboard", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() } } - fun getThemeAttrColor(context: Context, @AttrRes resId: Int): Int { - val typedValue = TypedValue() - context.theme.resolveAttribute(resId, typedValue, true) - val colorRes = typedValue.resourceId - var color = -1 - try { - color = context.resources.getColor(colorRes, context.theme) - } catch (e: Exception) { - e.printStackTrace() - } - - return color - } - fun bytesToMegabytes(bytes: Double): Double { return bytes / 1024 / 1024 } @@ -88,15 +64,30 @@ object AndroidUtils { else -> "$bytes B" } + fun openAppNotificationsSettings(context: Context) { + val packageName = context.packageName + + val intent = Intent("android.settings.APP_NOTIFICATION_SETTINGS") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.putExtra("android.provider.extra.APP_PACKAGE", packageName) + } else { + intent.putExtra("app_package", packageName) + intent.putExtra("app_uid", context.applicationInfo.uid) + } + context.startActivity(intent) + } + @Suppress("DEPRECATION") - fun isCanInstallUnknownApps(): Boolean { + fun isCanInstallUnknownApps(context: Context): Boolean { return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { Settings.Secure.getInt( - AppGlobal.Instance.contentResolver, + context.contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS ) == 1 } else { - AppGlobal.packageManager.canRequestPackageInstalls() + context.packageManager.canRequestPackageInstalls() } } @@ -130,20 +121,8 @@ object AndroidUtils { return intent } - fun getStatusBarInsets(insets: WindowInsetsCompat): Insets { - return insets.getInsets(WindowInsetsCompat.Type.statusBars()) - } - - fun getNavBarInsets(insets: WindowInsetsCompat): Insets { - return insets.getInsets(WindowInsetsCompat.Type.navigationBars()) - } - - fun getImeInsets(insets: WindowInsetsCompat): Insets { - return insets.getInsets(WindowInsetsCompat.Type.ime()) - } - - fun isBatterySaverOn(): Boolean { - return (AppGlobal.Instance.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode.isTrue + fun isBatterySaverOn(context: Context): Boolean { + return (context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode == true } fun getImageToShare(context: Context, existingFile: File): Uri? { @@ -226,3 +205,4 @@ sealed class ShareContent { data class TextWithImage(val text: String, val imageUri: Uri) : ShareContent() } + diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/util/TimeUtils.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/TimeUtils.kt new file mode 100644 index 00000000..468c439e --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/TimeUtils.kt @@ -0,0 +1,83 @@ +package com.meloda.app.fast.common.util + +import android.content.res.Resources +import com.conena.nanokt.jvm.util.dayOfMonth +import com.conena.nanokt.jvm.util.hour +import com.conena.nanokt.jvm.util.hourOfDay +import com.conena.nanokt.jvm.util.millisecond +import com.conena.nanokt.jvm.util.minute +import com.conena.nanokt.jvm.util.month +import com.conena.nanokt.jvm.util.second +import com.conena.nanokt.jvm.util.year +import com.meloda.app.fast.common.R +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +object TimeUtils { + + fun removeTime(date: Date): Long { + return Calendar.getInstance().apply { + time = date + hourOfDay = 0 + minute = 0 + second = 0 + millisecond = 0 + }.timeInMillis + } + + fun getLocalizedDate(resources: Resources, date: Long): String { + val now = Calendar.getInstance() + val then = Calendar.getInstance().also { it.timeInMillis = date } + + val pattern = when { + now.year != then.year -> "dd MMM yyyy" + now.month != then.month -> "dd MMMM" + now.dayOfMonth != then.dayOfMonth -> { + if (now.dayOfMonth - then.dayOfMonth == 1) { + return resources.getString(R.string.yesterday) + } else { + "dd MMMM" + } + } + + else -> return resources.getString(R.string.today) + } + + return SimpleDateFormat(pattern, Locale.getDefault()).format(date) + } + + fun getLocalizedTime(resources: Resources, date: Long): String { + val now = Calendar.getInstance() + val then = Calendar.getInstance().also { it.timeInMillis = date } + + return when { + now.year != then.year -> { + "${now.year - then.year}${resources.getString(R.string.year_short).lowercase()}" + } + + now.month != then.month -> { + "${now.month - then.month}${resources.getString(R.string.month_short).lowercase()}" + } + + now.dayOfMonth != then.dayOfMonth -> { + val change = now.dayOfMonth - then.dayOfMonth + + if (change % 7 == 0) { + "${change / 7}${resources.getString(R.string.week_short).lowercase()}" + } else { + "$change${resources.getString(R.string.day_short).lowercase()}" + } + } + + now.hour == then.hour && now.minute == then.minute -> { + resources.getString(R.string.time_now).lowercase() + } + + else -> { + SimpleDateFormat("HH:mm", Locale.getDefault()).format(date) + } + } + } +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/util/VkUtils.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/VkUtils.kt new file mode 100644 index 00000000..7389532f --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/VkUtils.kt @@ -0,0 +1,721 @@ +package com.meloda.app.fast.common.util + +//import android.content.Context +//import androidx.compose.ui.graphics.Color +//import androidx.compose.ui.text.AnnotatedString +//import androidx.compose.ui.text.SpanStyle +//import androidx.compose.ui.text.buildAnnotatedString +//import androidx.compose.ui.text.font.FontWeight +//import androidx.compose.ui.text.withStyle +//import com.meloda.app.fast.common.UiImage +//import com.meloda.app.fast.common.UiText +//import com.meloda.app.fast.common.extensions.orDots +//import com.meloda.app.fast.common.parseString +// +// +//@Suppress("MemberVisibilityCanBePrivate") +//object VkUtils { +// +// fun prepareMessageText(text: String, forConversations: Boolean = false): String { +// return text.apply { +// if (forConversations) { +// replace("\n", " ") +// } +// +// replace("&", "&") +// replace(""", "\"") +// replace("
", "\n") +// replace(">", ">") +// replace("<", "<") +// replace("
", "\n") +// replace("–", "-") +// trim() +// } +// } +// +// fun parseAttachments(baseAttachments: List?): List? { +// if (baseAttachments.isNullOrEmpty()) return null +// +// val attachments = mutableListOf() +// +// for (baseAttachment in baseAttachments) { +// when (baseAttachment.getPreparedType()) { +// AttachmentType.UNKNOWN -> continue +// +// AttachmentType.PHOTO -> { +// val photo = baseAttachment.photo ?: continue +// attachments += photo.toDomain() +// } +// +// AttachmentType.VIDEO -> { +// val video = baseAttachment.video ?: continue +// attachments += video.toDomain() +// } +// +// AttachmentType.AUDIO -> { +// val audio = baseAttachment.audio ?: continue +// attachments += audio.toDomain() +// } +// +// AttachmentType.FILE -> { +// val file = baseAttachment.file ?: continue +// attachments += file.toDomain() +// } +// +// AttachmentType.LINK -> { +// val link = baseAttachment.link ?: continue +// attachments += link.toDomain() +// } +// +// AttachmentType.MINI_APP -> { +// val miniApp = baseAttachment.miniApp ?: continue +// attachments += miniApp.toDomain() +// } +// +// AttachmentType.AUDIO_MESSAGE -> { +// val voiceMessage = baseAttachment.voiceMessage ?: continue +// attachments += voiceMessage.toDomain() +// } +// +// AttachmentType.STICKER -> { +// val sticker = baseAttachment.sticker ?: continue +// attachments += sticker.toDomain() +// } +// +// AttachmentType.GIFT -> { +// val gift = baseAttachment.gift ?: continue +// attachments += gift.toDomain() +// } +// +// AttachmentType.WALL -> { +// val wall = baseAttachment.wall ?: continue +// attachments += wall.toDomain() +// } +// +// AttachmentType.GRAFFITI -> { +// val graffiti = baseAttachment.graffiti ?: continue +// attachments += graffiti.toDomain() +// } +// +// AttachmentType.POLL -> { +// val poll = baseAttachment.poll ?: continue +// attachments += poll.toDomain() +// } +// +// AttachmentType.WALL_REPLY -> { +// val wallReply = baseAttachment.wallReply ?: continue +// attachments += wallReply.toDomain() +// } +// +// AttachmentType.CALL -> { +// val call = baseAttachment.call ?: continue +// attachments += call.toDomain() +// } +// +// AttachmentType.GROUP_CALL_IN_PROGRESS -> { +// val groupCall = baseAttachment.groupCall ?: continue +// attachments += groupCall.toDomain() +// } +// +// AttachmentType.CURATOR -> { +// val curator = baseAttachment.curator ?: continue +// attachments += curator.toDomain() +// } +// +// AttachmentType.EVENT -> { +// val event = baseAttachment.event ?: continue +// attachments += event.toDomain() +// } +// +// AttachmentType.STORY -> { +// val story = baseAttachment.story ?: continue +// attachments += story.toDomain() +// } +// +// AttachmentType.WIDGET -> { +// val widget = baseAttachment.widget ?: continue +// attachments += widget.toDomain() +// } +// +// AttachmentType.ARTIST -> { +// val artist = baseAttachment.artist ?: continue +// attachments += artist.toDomain() +// +// val audios = baseAttachment.audios ?: continue +// audios.map(VkAudioData::toDomain).let(attachments::addAll) +// } +// +// AttachmentType.AUDIO_PLAYLIST -> { +// val audioPlaylist = baseAttachment.audioPlaylist ?: continue +// attachments += audioPlaylist.toDomain() +// } +// +// AttachmentType.PODCAST -> { +// val podcast = baseAttachment.podcast ?: continue +// attachments += podcast.toDomain() +// } +// } +// } +// +// return attachments +// } +// +// fun getActionMessageText( +// context: Context, +// message: VkMessage?, +// youPrefix: String, +// messageUser: VkUserDomain?, +// messageGroup: VkGroupDomain?, +// action: VkMessage.Action?, +// actionUser: VkUserDomain?, +// actionGroup: VkGroupDomain?, +// ): AnnotatedString? { +// return when { +// message == null -> null +// action == null -> null +// +// else -> buildAnnotatedString { +// when (action) { +// VkMessage.Action.CHAT_CREATE -> { +// val text = message.actionText ?: return null +// +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// val string = UiText.ResourceParams( +// UiR.string.message_action_chat_created, +// listOf(prefix, text) +// ).parseString(context).orEmpty() +// +// append(string) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// +// val textStartIndex = string.indexOf(text) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = textStartIndex, +// end = textStartIndex + text.length +// ) +// } +// +// VkMessage.Action.CHAT_TITLE_UPDATE -> { +// val text = message.actionText ?: return null +// +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// val string = UiText.ResourceParams( +// UiR.string.message_action_chat_renamed, +// listOf(prefix, text) +// ).parseString(context).orEmpty() +// +// append(string) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// +// val textStartIndex = string.indexOf(text) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = textStartIndex, +// end = textStartIndex + text.length +// ) +// } +// +// VkMessage.Action.CHAT_PHOTO_UPDATE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_photo_update, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_PHOTO_REMOVE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_photo_remove, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_KICK_USER -> { +// val memberId = message.actionMemberId ?: return null +// val isUser = memberId > 0 +// val isGroup = memberId < 0 +// +// if (isUser && actionUser == null) return null +// if (isGroup && actionGroup == null) return null +// +// if (memberId == message.fromId) { +// val prefix = +// if (memberId == UserConfig.userId) youPrefix +// else actionUser.toString() +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_left, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } else { +// val prefix = +// if (message.fromId == UserConfig.userId) youPrefix +// else messageUser?.toString() ?: messageGroup?.toString().orDots() +// +// val postfix = +// if (memberId == UserConfig.userId) youPrefix.lowercase() +// else actionUser.toString() +// +// val string = UiText.ResourceParams( +// UiR.string.message_action_chat_user_kicked, +// listOf(prefix, postfix) +// ).parseString(context).orEmpty() +// +// append(string) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// +// val postfixStartIndex = string.indexOf(postfix) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = postfixStartIndex, +// end = postfixStartIndex + postfix.length +// ) +// } +// } +// +// VkMessage.Action.CHAT_INVITE_USER -> { +// val memberId = message.actionMemberId ?: 0 +// val isUser = memberId > 0 +// val isGroup = memberId < 0 +// +// if (isUser && actionUser == null) return null +// if (isGroup && actionGroup == null) return null +// +// if (memberId == message.fromId) { +// val prefix = +// if (memberId == UserConfig.userId) youPrefix +// else actionUser.toString() +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_returned, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } else { +// val prefix = +// if (message.fromId == UserConfig.userId) youPrefix +// else messageUser?.toString() ?: messageGroup?.toString().orDots() +// +// val postfix = +// if (memberId == UserConfig.userId) youPrefix.lowercase() +// else actionUser.toString() +// +// val string = UiText.ResourceParams( +// UiR.string.message_action_chat_user_invited, +// listOf(prefix, postfix) +// ).parseString(context).orEmpty() +// +// append(string) +// +// val postfixStartIndex = string.indexOf(postfix) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = postfixStartIndex, +// end = postfixStartIndex + postfix.length +// ) +// } +// } +// +// VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_joined_by_link, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_joined_by_call, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_joined_by_call_link, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_PIN_MESSAGE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_pin_message, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_UNPIN_MESSAGE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_unpin_message, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_SCREENSHOT -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_screenshot, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_STYLE_UPDATE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_style_update, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// } +// } +// } +// } +// +// fun getForwardsText(context: Context, message: VkMessage?): AnnotatedString? { +// return when { +// message == null -> null +// +// message.hasForwards() -> buildAnnotatedString { +// val forwards = message.forwards.orEmpty() +// +// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { +// append( +// UiText.Resource( +// if (forwards.size == 1) UiR.string.forwarded_message +// else UiR.string.forwarded_messages +// ).parseString(context) +// ) +// } +// } +// +// else -> null +// } +// } +// +// fun getAttachmentText( +// getText: (UiText) -> String, +// message: VkMessage? +// ): AnnotatedString? { +// return when { +// message == null -> null +// +// message.geoType != null -> buildAnnotatedString { +// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { +// when (message.geoType) { +// "point" -> getText(UiText.Resource(UiR.string.message_geo_point)) +// .let(::append) +// +// else -> getText(UiText.Resource(UiR.string.message_geo)) +// .let(::append) +// } +// } +// } +// +// message.hasAttachments() -> buildAnnotatedString { +// val attachments = message.attachments.orEmpty() +// +// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { +// if (attachments.size == 1) { +// getText(getAttachmentUiText(attachments.first())).let(::append) +// } else { +// when { +// isAttachmentsHaveOneType(attachments) -> { +// getText(getAttachmentUiText(attachments.first(), attachments.size)) +// .let(::append) +// } +// +// attachments.any { it.type == AttachmentType.ARTIST } -> { +// getText( +// getAttachmentUiText(attachments.first { it.type == AttachmentType.ARTIST }) +// ).let(::append) +// } +// +// else -> { +// getText(UiText.Resource(UiR.string.message_attachments_many)) +// .let(::append) +// } +// } +// } +// } +// } +// +// else -> null +// } +// } +// +// fun getAttachmentConversationIcon(message: VkMessage?): UiImage? { +// return message?.attachments?.let { attachments -> +// if (attachments.isEmpty()) return null +// if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) { +// message.geoType?.let { +// return UiImage.Resource(UiR.drawable.ic_map_marker) +// } +// +// getAttachmentIconByType(attachments.first().type) +// } else { +// UiImage.Resource(UiR.drawable.ic_baseline_attach_file_24) +// } +// } +// } +// +// +// +// fun getAttachmentUiText( +// attachment: VkAttachment, +// size: Int = 1, +// ): UiText { +// if (attachment.type.isMultiple()) { +// return when (attachment.type) { +// AttachmentType.PHOTO -> UiR.plurals.attachment_photos +// AttachmentType.VIDEO -> UiR.plurals.attachment_videos +// AttachmentType.AUDIO -> UiR.plurals.attachment_audios +// AttachmentType.FILE -> UiR.plurals.attachment_files +// else -> throw IllegalArgumentException("Unknown multiple type: ${attachment.type}") +// }.let { resId -> UiText.QuantityResource(resId, size) } +// } +// +// return when (attachment.type) { +// AttachmentType.UNKNOWN, +// AttachmentType.PHOTO, +// AttachmentType.VIDEO, +// AttachmentType.AUDIO, +// AttachmentType.FILE -> { +// throw IllegalArgumentException("Unknown multiple type: ${attachment.type}") +// } +// +// AttachmentType.LINK -> UiR.string.message_attachments_link +// AttachmentType.AUDIO_MESSAGE -> UiR.string.message_attachments_audio_message +// AttachmentType.MINI_APP -> UiR.string.message_attachments_mini_app +// AttachmentType.STICKER -> UiR.string.message_attachments_sticker +// AttachmentType.GIFT -> UiR.string.message_attachments_gift +// AttachmentType.WALL -> UiR.string.message_attachments_wall +// AttachmentType.GRAFFITI -> UiR.string.message_attachments_graffiti +// AttachmentType.POLL -> UiR.string.message_attachments_poll +// AttachmentType.WALL_REPLY -> UiR.string.message_attachments_wall_reply +// AttachmentType.CALL -> UiR.string.message_attachments_call +// AttachmentType.GROUP_CALL_IN_PROGRESS -> UiR.string.message_attachments_call_in_progress +// AttachmentType.CURATOR -> UiR.string.message_attachments_curator +// AttachmentType.EVENT -> UiR.string.message_attachments_event +// AttachmentType.STORY -> UiR.string.message_attachments_story +// AttachmentType.WIDGET -> UiR.string.message_attachments_widget +// AttachmentType.ARTIST -> UiR.string.message_attachments_artist +// AttachmentType.AUDIO_PLAYLIST -> UiR.string.message_attachments_audio_playlist +// AttachmentType.PODCAST -> UiR.string.message_attachments_podcast +// }.let(UiText::Resource) +// } +// +// fun getTextWithVisualizedMentions( +// originalText: String, +// mentionColor: Color, +// ): AnnotatedString = buildAnnotatedString { +// val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex() +// +// val mentions = mutableListOf() +// +// var currentIndex = 0 +// val replacements = mutableListOf>() +// +// // TODO: 25/04/2024, Danil Nikolaev: check why not working ([id279494346|@iworld2rist] да убери ты Елену Шлипс от меня) +// val result = regex.replace(originalText) { matchResult -> +// val idPrefix = matchResult.groups[1]?.value.orEmpty() +// val startIndex = matchResult.range.first +// val endIndex = matchResult.range.last +// +// val id = matchResult.groups[2]?.value ?: "" +// val text = matchResult.groups[3]?.value ?: "" +// +// val replaced = +// text.substring(startIndex, endIndex + 1) +// .replace("[$idPrefix$id|$text]", text) +// +// val indexRange = +// (startIndex + currentIndex)..startIndex + currentIndex + replaced.length +// +// replacements.add(indexRange to replaced) +// +// mentions += MentionIndex( +// id = id.toIntOrNull() ?: -1, +// idPrefix = idPrefix, +// indexRange = indexRange +// ) +// +// currentIndex += replaced.length - (endIndex - startIndex + 1) +// +// replaced +// } +// +// append(result) +// +// mentions.forEach { mention -> +// val startIndex = mention.indexRange.first +// val endIndex = mention.indexRange.last +// +// addStyle( +// style = SpanStyle(color = mentionColor), +// start = startIndex, +// end = endIndex +// ) +// addStringAnnotation( +// tag = mention.idPrefix, +// annotation = mention.id.toString(), +// start = startIndex, +// end = endIndex +// ) +// } +// } +// +// +//} diff --git a/core/common/src/main/res/values/strings.xml b/core/common/src/main/res/values/strings.xml new file mode 100644 index 00000000..8b98b604 --- /dev/null +++ b/core/common/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + Yesterday + Today + Y + M + W + D + Now + diff --git a/core/data/.gitignore b/core/data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 00000000..73ebf969 --- /dev/null +++ b/core/data/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) +} + +group = "com.meloda.app.fast.data" + +android { + namespace = "com.meloda.app.fast.data" + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + } +} + +dependencies { + api(projects.core.common) + api(projects.core.datastore) + api(projects.core.model) + api(projects.core.network) + api(projects.core.database) + + implementation(libs.koin.android) + + // TODO: 05/05/2024, Danil Nikolaev: research, maybe remove + implementation(libs.retrofit) + implementation(libs.eithernet) +} diff --git a/core/data/src/main/AndroidManifest.xml b/core/data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/data/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt new file mode 100644 index 00000000..59c7638f --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt @@ -0,0 +1,354 @@ +package com.meloda.app.fast.data + +import android.util.Log +import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.common.extensions.asInt +import com.meloda.app.fast.common.extensions.listenValue +import com.meloda.app.fast.common.extensions.toList +import com.meloda.app.fast.data.api.messages.MessagesUseCase +import com.meloda.app.fast.model.ApiEvent +import com.meloda.app.fast.model.InteractionType +import com.meloda.app.fast.model.LongPollEvent +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class LongPollUpdatesParser( + private val messagesUseCase: MessagesUseCase +) { + private val job = SupervisorJob() + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.d("LongPollUpdatesParser", "error: $throwable") + throwable.printStackTrace() + } + + private val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job + exceptionHandler + + private val coroutineScope = CoroutineScope(coroutineContext) + + private val listenersMap: MutableMap>> = + mutableMapOf() + + fun parseNextUpdate(event: List) { + val eventId = event.first().asInt() + + val eventType: ApiEvent = try { + ApiEvent.parse(eventId) + } catch (e: Exception) { + e.printStackTrace() + Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") + return + } + + when (eventType) { + ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) + ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) + ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event) + ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event) + ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event) + ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event) + ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event) + ApiEvent.PIN_UNPIN_CONVERSATION -> parseConversationPinStateChanged(eventType, event) + + ApiEvent.TYPING, + ApiEvent.AUDIO_MESSAGE_RECORDING, + ApiEvent.PHOTO_UPLOADING, + ApiEvent.VIDEO_UPLOADING, + ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event) + + ApiEvent.UNREAD_COUNT_UPDATE -> onNewEvent(eventType, event) + } + } + + private fun onNewEvent(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event") + } + + private fun parseInteraction(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val interactionType = when (eventType) { + ApiEvent.TYPING -> InteractionType.Typing + ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage + ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo + ApiEvent.VIDEO_UPLOADING -> InteractionType.Video + ApiEvent.FILE_UPLOADING -> InteractionType.File + else -> return + } + + val peerId = event[1].asInt() + val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId } + val totalCount = event[3].asInt() + val timestamp = event[4].asInt() + + // if userIds contains only account's id, then we don't need to show our status + if (userIds.isEmpty()) return + + coroutineScope.launch { + listenersMap[eventType]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.Interaction( + interactionType = interactionType, + peerId = peerId, + userIds = userIds, + totalCount = totalCount, + timestamp = timestamp + ) + ) + } + } + } + } + + private fun parseConversationPinStateChanged(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asInt() + val majorId = event[2].asInt() + + coroutineScope.launch { + listenersMap[ApiEvent.PIN_UNPIN_CONVERSATION]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.VkConversationPinStateChangedEvent( + peerId = peerId, + majorId = majorId + ) + ) + } + } + } + } + + private fun parseMessageSetFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + } + + private fun parseMessageClearFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + } + + private fun parseMessageNew(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val messageId = event[1].asInt() + + coroutineScope.launch(Dispatchers.IO) { + val newMessageEvent: LongPollEvent.VkMessageNewEvent? = + loadNormalMessage( + eventType, + messageId + ) + + newMessageEvent?.let { event -> + listenersMap[ApiEvent.MESSAGE_NEW]?.let { + it.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent(event) + } + } + } + } + } + + private fun parseMessageEdit(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val messageId = event[1].asInt() + + coroutineScope.launch { + val editedMessageEvent: LongPollEvent.VkMessageEditEvent? = + loadNormalMessage( + eventType, + messageId + ) + + editedMessageEvent?.let { event -> + listenersMap[ApiEvent.MESSAGE_EDIT]?.let { + it.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent(event) + } + } + } + } + } + + private fun parseMessageReadIncoming(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val peerId = event[1].asInt() + val messageId = event[2].asInt() + val unreadCount = event[3].asInt() + + coroutineScope.launch { + listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.VkMessageReadIncomingEvent( + peerId = peerId, + messageId = messageId, + unreadCount = unreadCount + ) + ) + } + } + } + } + + private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val peerId = event[1].asInt() + val messageId = event[2].asInt() + val unreadCount = event[3].asInt() + + coroutineScope.launch { + listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.VkMessageReadOutgoingEvent( + peerId = peerId, + messageId = messageId, + unreadCount = unreadCount + ) + ) + } + } + } + } + + private fun parseMessagesDeleted(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + } + + private suspend fun loadNormalMessage( + eventType: ApiEvent, + messageId: Int + ): T? = suspendCoroutine { + coroutineScope.launch(Dispatchers.IO) { + messagesUseCase.getById( + messageId = messageId, + extended = true, + fields = VkConstants.ALL_FIELDS + ).listenValue(this) { state -> + state.processState( + error = { error -> + Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error") + }, + success = { response -> + response?.let { message -> + VkMemoryCache[message.id] = message + messagesUseCase.storeMessage(message) + + val resumeValue: LongPollEvent? = when (eventType) { + ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message) + ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message) + + else -> null + } + + resumeValue?.let { value -> it.resume(value as T) } + } ?: it.resume(null) + } + ) + } + } + } + + private fun registerListener( + eventType: ApiEvent, + listener: VkEventCallback + ) { + listenersMap.let { map -> + map[eventType] = (map[eventType] ?: mutableListOf()).also { it.add(listener) } + } + } + + private fun registerListeners( + eventTypes: List, + listener: VkEventCallback + ) { + eventTypes.forEach { eventType -> registerListener(eventType, listener) } + } + + fun onConversationPinStateChanged(listener: VkEventCallback) { + registerListener(ApiEvent.PIN_UNPIN_CONVERSATION, listener) + } + + fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) { + onConversationPinStateChanged(assembleEventCallback(block)) + } + + fun onMessageIncomingRead(listener: VkEventCallback) { + registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener) + } + + fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) { + onMessageIncomingRead(assembleEventCallback(block)) + } + + fun onMessageOutgoingRead(listener: VkEventCallback) { + registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener) + } + + fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) { + onMessageOutgoingRead(assembleEventCallback(block)) + } + + fun onNewMessage(listener: VkEventCallback) { + registerListener(ApiEvent.MESSAGE_NEW, listener) + } + + fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) { + onNewMessage(assembleEventCallback(block)) + } + + fun onMessageEdited(listener: VkEventCallback) { + registerListener(ApiEvent.MESSAGE_EDIT, listener) + } + + fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) { + onMessageEdited(assembleEventCallback(block)) + } + + fun onInteractions(listener: VkEventCallback) { + registerListeners( + eventTypes = listOf( + ApiEvent.TYPING, + ApiEvent.AUDIO_MESSAGE_RECORDING, + ApiEvent.PHOTO_UPLOADING, + ApiEvent.VIDEO_UPLOADING, + ApiEvent.FILE_UPLOADING + ), + listener = listener + ) + } + + fun onInteractions(block: (LongPollEvent.Interaction) -> Unit) { + onInteractions(assembleEventCallback(block)) + } + + fun clearListeners() { + listenersMap.clear() + } +} + +internal inline fun assembleEventCallback( + crossinline block: (R) -> Unit, +): VkEventCallback { + return VkEventCallback { event -> block.invoke(event) } +} + +fun interface VkEventCallback { + fun onEvent(event: T) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCase.kt new file mode 100644 index 00000000..dd5acee8 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCase.kt @@ -0,0 +1,23 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import kotlinx.coroutines.flow.Flow + +interface LongPollUseCase { + + fun getLongPollServer( + needPts: Boolean, + version: Int + ): Flow> + + fun getLongPollUpdates( + serverUrl: String, + act: String = "a_check", + key: String, + ts: Int, + wait: Int, + mode: Int, + version: Int + ): Flow> +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCaseImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCaseImpl.kt new file mode 100644 index 00000000..568d36c6 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCaseImpl.kt @@ -0,0 +1,49 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.data.api.longpoll.LongPollRepository +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class LongPollUseCaseImpl( + private val repository: LongPollRepository +) : LongPollUseCase { + + override fun getLongPollServer( + needPts: Boolean, + version: Int + ): Flow> = flow { + emit(State.Loading) + + val newState = repository.getLongPollServer( + needPts = needPts, + version = version + ).mapToState() + + emit(newState) + } + + override fun getLongPollUpdates( + serverUrl: String, + act: String, + key: String, + ts: Int, + wait: Int, + mode: Int, + version: Int + ): Flow> = flow { + emit(State.Loading) + + val newState = repository.getLongPollUpdates( + serverUrl, + act = act, + key = key, + ts = ts, + wait = wait, + mode = mode, + version = version + ).mapToState() + emit(newState) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt new file mode 100644 index 00000000..bf3b663d --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt @@ -0,0 +1,76 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.network.OAuthErrorDomain +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map + +sealed class State { + + data object Idle : State() + data class Success(val data: T) : State() + data object Loading : State() + + sealed class Error : State() { + + data class ApiError( + val errorCode: Int, + val errorMessage: String, + ) : Error() + + data object ConnectionError : Error() + + data object Unknown : Error() + + data object InternalError : Error() + + data class OAuthError(val error: OAuthErrorDomain) : Error() + } + + fun isLoading(): Boolean = this is Loading + + companion object { + + val UNKNOWN_ERROR = Error.Unknown + } +} + +inline fun State.processState( + error: (error: State.Error) -> (Unit), + success: (data: T) -> (Unit), + idle: (() -> (Unit)) = {}, + loading: (() -> (Unit)) = {}, +) { + when (this) { + is State.Error -> error(this) + State.Idle -> idle() + State.Loading -> loading() + is State.Success -> success(data) + } +} + +inline fun Flow>.mapSuccess( + crossinline transform: suspend (value: T) -> R +): Flow = filterIsInstance>() + .map { state -> transform.invoke(state.data) } + +fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) { + null -> State.Error.ConnectionError + else -> State.Error.ApiError(code, message) +} + +fun OAuthErrorDomain?.toStateApiError(): State.Error = when (this) { + null -> State.Error.ConnectionError + else -> State.Error.OAuthError(this) +} + +fun ApiResult.mapToState() = when (this) { + is ApiResult.Success -> State.Success(this.value) + + is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError + is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR + is ApiResult.Failure.HttpFailure -> this.error.toStateApiError() + is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/VkGroupsMap.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkGroupsMap.kt new file mode 100644 index 00000000..3c92b118 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkGroupsMap.kt @@ -0,0 +1,47 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.model.api.data.VkMessageData +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkGroupDomain +import com.meloda.app.fast.model.api.domain.VkMessage +import kotlin.math.abs + +class VkGroupsMap( + private val groups: List +) { + + private val map: HashMap by lazy { + HashMap(groups.associateBy(VkGroupDomain::id)) + } + + fun groups(): List = map.values.toList() + + fun conversationGroup(conversation: VkConversation): VkGroupDomain? = + if (!conversation.peerType.isGroup()) null + else map[abs(conversation.id)] + + fun messageActionGroup(message: VkMessage): VkGroupDomain? = + if (message.actionMemberId == null || message.actionMemberId!! >= 0) null + else map[abs(message.actionMemberId!!)] + + fun messageActionGroup(message: VkMessageData): VkGroupDomain? = + if (message.action?.memberId == null || message.action!!.memberId!! >= 0) null + else map[abs(message.action!!.memberId!!)] + + fun messageGroup(message: VkMessage): VkGroupDomain? = + if (!message.isGroup()) null + else map[abs(message.fromId)] + + fun messageGroup(message: VkMessageData): VkGroupDomain? = + if (message.fromId >= 0) null + else map[abs(message.fromId)] + + fun group(groupId: Int): VkGroupDomain? = map[abs(groupId)] + + companion object { + + fun forGroups(groups: List): VkGroupsMap = VkGroupsMap(groups = groups) + + fun List.toGroupsMap(): VkGroupsMap = forGroups(this) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/VkMemoryCache.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkMemoryCache.kt new file mode 100644 index 00000000..4f3bf277 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkMemoryCache.kt @@ -0,0 +1,119 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.model.api.domain.VkContactDomain +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkGroupDomain +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.model.api.domain.VkUser +import kotlin.math.abs + +object VkMemoryCache { + + private val users: HashMap = hashMapOf() + private val groups: HashMap = hashMapOf() + private val messages: HashMap = hashMapOf() + private val conversations: HashMap = hashMapOf() + private val contacts: HashMap = hashMapOf() + + fun appendUsers(users: List) { + users.forEach { user -> VkMemoryCache.users[user.id] = user } + } + + fun appendGroups(groups: List) { + groups.forEach { group -> VkMemoryCache.groups[abs(group.id)] = group } + } + + fun appendMessages(messages: List) { + messages.forEach { message -> VkMemoryCache.messages[message.id] = message } + } + + fun appendConversations(conversations: List) { + conversations.forEach { conversation -> + VkMemoryCache.conversations[conversation.id] = conversation + } + } + + fun appendContacts(contacts: List) { + contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact } + } + + operator fun set(userId: Int, user: VkUser) { + users[userId] = user + } + + operator fun set(groupId: Int, group: VkGroupDomain) { + groups[groupId] = group + } + + operator fun set(messageId: Int, message: VkMessage) { + messages[messageId] = message + } + + operator fun set(conversationId: Int, conversation: VkConversation) { + conversations[conversationId] = conversation + } + + operator fun set(contactId: Int, contact: VkContactDomain) { + contacts[contactId] = contact + } + + fun getUser(id: Int): VkUser? { + return getUsers(id).firstOrNull() + } + + fun getUsers(vararg ids: Int): List { + return getUsers(ids.toList()) + } + + fun getUsers(ids: List): List { + return ids.mapNotNull { id -> users[id] } + } + + fun getGroup(id: Int): VkGroupDomain? { + return getGroups(id).firstOrNull() + } + + fun getGroups(vararg ids: Int): List { + return getGroups(ids.toList()) + } + + fun getGroups(ids: List): List { + return ids.mapNotNull { id -> groups[id] } + } + + fun getMessage(id: Int): VkMessage? { + return getMessages(id).firstOrNull() + } + + fun getMessages(vararg ids: Int): List { + return getMessages(ids.toList()) + } + + fun getMessages(ids: List): List { + return ids.mapNotNull { id -> messages[id] } + } + + fun getConversation(id: Int): VkConversation? { + return getConversations(id).firstOrNull() + } + + fun getConversations(vararg ids: Int): List { + return getConversations(ids.toList()) + } + + fun getConversations(ids: List): List { + return ids.mapNotNull { id -> conversations[id] } + } + + fun getContact(id: Int): VkContactDomain? { + return getContacts(id).firstOrNull() + } + + fun getContacts(vararg ids: Int): List { + return getContacts(ids.toList()) + } + + fun getContacts(ids: List): List { + return ids.mapNotNull { id -> contacts[id] } + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/VkUsersMap.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkUsersMap.kt new file mode 100644 index 00000000..60c14f91 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkUsersMap.kt @@ -0,0 +1,46 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.model.api.data.VkMessageData +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.model.api.domain.VkUser + +class VkUsersMap( + private val users: List +) { + + private val map: HashMap by lazy { + HashMap(users.associateBy(VkUser::id)) + } + + fun users(): List = map.values.toList() + + fun conversationUser(conversation: VkConversation): VkUser? = + if (!conversation.peerType.isUser()) null + else map[conversation.id] + + fun messageActionUser(message: VkMessage): VkUser? = + if (message.actionMemberId == null || message.actionMemberId!! <= 0) null + else map[message.actionMemberId] + + fun messageActionUser(message: VkMessageData): VkUser? = + if (message.action?.memberId == null || message.action!!.memberId!! <= 0) null + else map[message.action!!.memberId] + + fun messageUser(message: VkMessage): VkUser? = + if (!message.isUser()) null + else map[message.fromId] + + fun messageUser(message: VkMessageData): VkUser? = + if (message.fromId > 0) map[message.fromId] + else null + + fun user(userId: Int): VkUser? = map[userId] + + companion object { + + fun forUsers(users: List): VkUsersMap = VkUsersMap(users = users) + + fun List.toUsersMap(): VkUsersMap = forUsers(this) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepository.kt new file mode 100644 index 00000000..4c0d052c --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepository.kt @@ -0,0 +1,15 @@ +package com.meloda.app.fast.data.api.account + +import com.meloda.app.fast.model.api.requests.AccountSetOfflineRequest +import com.meloda.app.fast.model.api.requests.AccountSetOnlineRequest + +interface AccountRepository { + + suspend fun setOnline( + params: AccountSetOnlineRequest + ): Boolean + + suspend fun setOffline( + params: AccountSetOfflineRequest + ): Boolean +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepositoryImpl.kt new file mode 100644 index 00000000..2d6a53cd --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepositoryImpl.kt @@ -0,0 +1,19 @@ +package com.meloda.app.fast.data.api.account + +import com.meloda.app.fast.model.api.requests.AccountSetOfflineRequest +import com.meloda.app.fast.model.api.requests.AccountSetOnlineRequest +import com.meloda.app.fast.network.service.account.AccountService + +// TODO: 05/05/2024, Danil Nikolaev: implement +class AccountRepositoryImpl( + private val accountService: AccountService +) : com.meloda.app.fast.data.api.account.AccountRepository { + + override suspend fun setOnline(params: AccountSetOnlineRequest): Boolean { + return false + } + + override suspend fun setOffline(params: AccountSetOfflineRequest): Boolean { + return false + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCase.kt new file mode 100644 index 00000000..c52c312f --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCase.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.data.api.account + +import com.meloda.app.fast.data.State +import kotlinx.coroutines.flow.Flow + +interface AccountUseCase { + + suspend fun setOnline( + voip: Boolean, + accessToken: String + ): Flow> + + suspend fun setOffline( + accessToken: String + ): Flow> +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCaseImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCaseImpl.kt new file mode 100644 index 00000000..7bdafefc --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCaseImpl.kt @@ -0,0 +1,49 @@ +package com.meloda.app.fast.data.api.account + +import com.meloda.app.fast.data.State +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +// TODO: 05/05/2024, Danil Nikolaev: implement +class AccountUseCaseImpl( + private val accountRepository: com.meloda.app.fast.data.api.account.AccountRepository +) : com.meloda.app.fast.data.api.account.AccountUseCase { + + override suspend fun setOnline( + voip: Boolean, + accessToken: String + ): Flow> = flow { +// emit(com.meloda.app.fast.data.State.Loading) +// +// val newState = accountRepository.setOnline( +// params = AccountSetOnlineRequest( +// voip = voip, +// accessToken = accessToken +// ) +// ).fold( +// onSuccess = { response -> com.meloda.app.fast.data.State.Success(response) }, +// onNetworkFailure = { com.meloda.app.fast.data.State.Error.ConnectionError }, +// onUnknownFailure = { com.meloda.app.fast.data.State.UNKNOWN_ERROR }, +// onHttpFailure = { result -> result.error.toStateApiError() }, +// onApiFailure = { result -> result.error.toStateApiError() } +// ) +// emit(newState) + } + + override suspend fun setOffline( + accessToken: String + ): Flow> = flow { + emit(com.meloda.app.fast.data.State.Loading) + +// val newState = accountRepository.setOffline( +// params = AccountSetOfflineRequest(accessToken = accessToken) +// ).fold( +// onSuccess = { response -> com.meloda.app.fast.data.State.Success(response) }, +// onNetworkFailure = { com.meloda.app.fast.data.State.Error.ConnectionError }, +// onUnknownFailure = { com.meloda.app.fast.data.State.UNKNOWN_ERROR }, +// onHttpFailure = { result -> result.error.toStateApiError() }, +// onApiFailure = { result -> result.error.toStateApiError() } +// ) +// emit(newState) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/audios/AudiosRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/audios/AudiosRepository.kt new file mode 100644 index 00000000..33df923a --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/audios/AudiosRepository.kt @@ -0,0 +1,26 @@ +package com.meloda.app.fast.data.api.audios + +import com.meloda.app.fast.model.api.responses.AudiosGetUploadServerResponse +import com.meloda.app.fast.network.ApiResponse +import com.meloda.app.fast.network.RestApiError +import com.meloda.app.fast.network.service.audios.AudiosService +import com.slack.eithernet.ApiResult +import okhttp3.MultipartBody + +class AudiosRepository( + private val audiosService: AudiosService +) { + + suspend fun getUploadServer(): ApiResult, RestApiError> = + audiosService.getUploadServer() + + suspend fun upload(url: String, file: MultipartBody.Part) = audiosService.upload(url, file) + + suspend fun save(server: Int, audio: String, hash: String) = audiosService.save( + mapOf( + "server" to server.toString(), + "audio" to audio, + "hash" to hash + ) + ) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepository.kt new file mode 100644 index 00000000..6ebfc4fa --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepository.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.data.api.auth + +import com.meloda.app.fast.model.api.requests.AuthDirectRequest +import com.meloda.app.fast.model.api.responses.AuthDirectResponse +import com.meloda.app.fast.model.api.responses.SendSmsResponse +import com.meloda.app.fast.network.OAuthErrorDomain +import com.slack.eithernet.ApiResult + +interface AuthRepository { + +// suspend fun auth( +// params: AuthDirectRequest +// ): ApiResult + + suspend fun sendSms( + validationSid: String + ): SendSmsResponse +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepositoryImpl.kt new file mode 100644 index 00000000..f6489814 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepositoryImpl.kt @@ -0,0 +1,35 @@ +package com.meloda.app.fast.data.api.auth + +import com.meloda.app.fast.model.api.requests.AuthDirectRequest +import com.meloda.app.fast.model.api.responses.AuthDirectResponse +import com.meloda.app.fast.model.api.responses.SendSmsResponse +import com.meloda.app.fast.network.OAuthErrorDomain +import com.meloda.app.fast.network.service.auth.AuthService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AuthRepositoryImpl( + private val authService: AuthService +) : AuthRepository { + +// override suspend fun auth( +// params: AuthDirectRequest +// ): ApiResult { +// +// } + + // TODO: 05/05/2024, Danil Nikolaev: implement + override suspend fun sendSms( + validationSid: String + ): SendSmsResponse = withContext(Dispatchers.IO) { + SendSmsResponse( + validationSid = null, delay = null, validationType = null, validationResend = null + + ) +// authService.sendSms(validationSid).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepository.kt new file mode 100644 index 00000000..de2f64f6 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepository.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.data.api.conversations + +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult + +interface ConversationsRepository { + + suspend fun getConversations( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> + + suspend fun storeConversations(conversations: List) + suspend fun delete(peerId: Int): ApiResult + suspend fun pin(peerId: Int): ApiResult + suspend fun unpin(peerId: Int): ApiResult +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepositoryImpl.kt new file mode 100644 index 00000000..2af0c929 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepositoryImpl.kt @@ -0,0 +1,109 @@ +package com.meloda.app.fast.data.api.conversations + +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.data.VkGroupsMap +import com.meloda.app.fast.data.VkMemoryCache +import com.meloda.app.fast.data.VkUsersMap +import com.meloda.app.fast.database.dao.ConversationDao +import com.meloda.app.fast.model.api.data.VkContactData +import com.meloda.app.fast.model.api.data.VkGroupData +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.data.asDomain +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.asEntity +import com.meloda.app.fast.model.api.requests.ConversationsDeleteRequest +import com.meloda.app.fast.model.api.requests.ConversationsGetRequest +import com.meloda.app.fast.model.api.requests.ConversationsPinRequest +import com.meloda.app.fast.model.api.requests.ConversationsUnpinRequest +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiDefault +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.service.conversations.ConversationsService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ConversationsRepositoryImpl( + private val conversationsService: ConversationsService, + private val conversationDao: ConversationDao +) : ConversationsRepository { + + override suspend fun getConversations( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestModel = ConversationsGetRequest( + count = count, + offset = offset, + fields = VkConstants.ALL_FIELDS, + filter = "all", + extended = true, + startMessageId = null + ) + + conversationsService.getConversations(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) + val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain) + val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain) + + val usersMap = VkUsersMap.forUsers(profilesList) + val groupsMap = VkGroupsMap.forGroups(groupsList) + + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + response.items.map { item -> + val lastMessage = item.lastMessage?.asDomain()?.let { message -> + message.copy( + user = usersMap.messageUser(message), + group = groupsMap.messageGroup(message), + actionUser = usersMap.messageActionUser(message), + actionGroup = groupsMap.messageActionGroup(message) + ).also { VkMemoryCache[message.id] = it } + } + item.conversation.asDomain(lastMessage).let { conversation -> + conversation.copy( + user = usersMap.conversationUser(conversation), + group = groupsMap.conversationGroup(conversation) + ).also { VkMemoryCache[conversation.id] = it } + } + } + }, + errorMapper = { error -> + error?.toDomain() + } + ) + } + + override suspend fun storeConversations(conversations: List) { + conversationDao.insertAll(conversations.map(VkConversation::asEntity)) + } + + override suspend fun delete(peerId: Int): ApiResult = + withContext(Dispatchers.IO) { + val requestModel = ConversationsDeleteRequest(peerId = peerId) + + conversationsService.delete(requestModel.map).mapApiResult( + successMapper = { response -> response.requireResponse().lastDeletedId }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun pin( + peerId: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = ConversationsPinRequest(peerId = peerId) + conversationsService.pin(requestModel.map).mapApiDefault() + } + + override suspend fun unpin( + peerId: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = ConversationsUnpinRequest(peerId = peerId) + conversationsService.unpin(requestModel.map).mapApiDefault() + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsUseCase.kt new file mode 100644 index 00000000..e6663ea5 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsUseCase.kt @@ -0,0 +1,19 @@ +package com.meloda.app.fast.data.api.conversations + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.api.domain.VkConversation +import kotlinx.coroutines.flow.Flow + +interface ConversationsUseCase { + + fun getConversations( + count: Int?, + offset: Int?, + ): Flow>> + + fun delete(peerId: Int): Flow> + + fun changePinState(peerId: Int, pin: Boolean): Flow> + + suspend fun storeConversations(conversations: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/files/FilesRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/files/FilesRepository.kt new file mode 100644 index 00000000..57930be2 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/files/FilesRepository.kt @@ -0,0 +1,31 @@ +package com.meloda.app.fast.data.api.files + +import com.meloda.app.fast.network.service.files.FilesService +import okhttp3.MultipartBody + +class FilesRepository( + private val filesService: FilesService +) { + + // TODO: 05/05/2024, Danil Nikolaev: reimplement +// enum class FileType(val value: String) { +// @Json(name = "doc") +// FILE("doc"), +// +// @Json(name = "audio_message") +// AUDIO_MESSAGE("audio_message") +// } +// +// suspend fun getMessagesUploadServer(peerId: Int, type: FileType) = +// filesService.getUploadServer( +// mapOf( +// "peer_id" to peerId.toString(), +// "type" to type.value +// ) +// ) + + suspend fun uploadFile(url: String, file: MultipartBody.Part) = filesService.upload(url, file) + + suspend fun saveMessageFile(file: String) = filesService.save(mapOf("file" to file)) + +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepository.kt new file mode 100644 index 00000000..498cbfab --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepository.kt @@ -0,0 +1,26 @@ +package com.meloda.app.fast.data.api.friends + +import com.meloda.app.fast.model.FriendsInfo +import com.meloda.app.fast.model.api.domain.VkUser +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult + +interface FriendsRepository { + + suspend fun getAllFriends( + count: Int?, + offset: Int? + ): ApiResult + + suspend fun getFriends( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> + + suspend fun getOnlineFriends( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> + + suspend fun storeUsers(users: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepositoryImpl.kt new file mode 100644 index 00000000..0c43874b --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepositoryImpl.kt @@ -0,0 +1,83 @@ +package com.meloda.app.fast.data.api.friends + +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.data.VkMemoryCache +import com.meloda.app.fast.database.dao.UsersDao +import com.meloda.app.fast.model.FriendsInfo +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.domain.VkUser +import com.meloda.app.fast.model.api.domain.asEntity +import com.meloda.app.fast.model.api.requests.GetFriendsRequest +import com.meloda.app.fast.model.api.requests.GetOnlineFriendsRequest +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiDefault +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.service.friends.FriendsService +import com.slack.eithernet.ApiResult +import com.slack.eithernet.successOrElse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext + +class FriendsRepositoryImpl( + private val service: FriendsService, + private val dao: UsersDao +) : FriendsRepository { + + override suspend fun getAllFriends( + count: Int?, + offset: Int? + ): ApiResult = withContext(Dispatchers.IO) { + val friends = async { getFriends(count, offset) }.await() + .successOrElse { failure -> + return@withContext failure + } + + val onlineFriends = async { getOnlineFriends(count, offset) }.await() + .successOrElse { failure -> + return@withContext failure + }.mapNotNull { userId -> friends.find { it.id == userId } } + + ApiResult.success(FriendsInfo(friends, onlineFriends)) + } + + override suspend fun getFriends( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestModel = GetFriendsRequest( + order = "hints", + count = count, + offset = offset, + fields = VkConstants.USER_FIELDS + ) + service.getFriends(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + val users = response.items.map(VkUserData::mapToDomain) + + VkMemoryCache.appendUsers(users) + + users + }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun getOnlineFriends( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestModel = GetOnlineFriendsRequest( + order = "hints", + count = count, + offset = offset, + ) + + service.getOnlineFriends(requestModel.map).mapApiDefault() + } + + override suspend fun storeUsers(users: List) = withContext(Dispatchers.IO) { + dao.insertAll(users.map(VkUser::asEntity)) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsUseCase.kt new file mode 100644 index 00000000..90749324 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsUseCase.kt @@ -0,0 +1,26 @@ +package com.meloda.app.fast.data.api.friends + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.FriendsInfo +import com.meloda.app.fast.model.api.domain.VkUser +import kotlinx.coroutines.flow.Flow + +interface FriendsUseCase { + + fun getAllFriends( + count: Int?, + offset: Int? + ): Flow> + + fun getFriends( + count: Int?, + offset: Int? + ): Flow>> + + fun getOnlineFriends( + count: Int?, + offset: Int? + ): Flow>> + + suspend fun storeUsers(users: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepository.kt new file mode 100644 index 00000000..16033e7c --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepository.kt @@ -0,0 +1,24 @@ +package com.meloda.app.fast.data.api.longpoll + +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult + +interface LongPollRepository { + + suspend fun getLongPollServer( + needPts: Boolean, + version: Int + ): ApiResult + + suspend fun getLongPollUpdates( + serverUrl: String, + act: String, + key: String, + ts: Int, + wait: Int, + mode: Int, + version: Int + ): ApiResult +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepositoryImpl.kt new file mode 100644 index 00000000..ff054f75 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepositoryImpl.kt @@ -0,0 +1,57 @@ +package com.meloda.app.fast.data.api.longpoll + +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import com.meloda.app.fast.model.api.requests.LongPollGetUpdatesRequest +import com.meloda.app.fast.model.api.requests.MessagesGetLongPollServerRequest +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.mapResult +import com.meloda.app.fast.network.service.longpoll.LongPollService +import com.meloda.app.fast.network.service.messages.MessagesService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class LongPollRepositoryImpl( + private val longPollService: LongPollService, + private val messagesService: MessagesService +) : LongPollRepository { + + override suspend fun getLongPollServer( + needPts: Boolean, + version: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesGetLongPollServerRequest( + needPts = needPts, + version = version + ) + messagesService.getLongPollServer(requestModel.map).mapApiResult( + successMapper = { response -> response.requireResponse() }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun getLongPollUpdates( + serverUrl: String, + act: String, + key: String, + ts: Int, + wait: Int, + mode: Int, + version: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = LongPollGetUpdatesRequest( + act = act, + key = key, + ts = ts, + wait = wait, + mode = mode, + version = version + ) + longPollService.getResponse(serverUrl, requestModel.map).mapResult( + successMapper = { response -> response }, + errorMapper = { error -> error?.toDomain() } + ) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt new file mode 100644 index 00000000..9d3cd7ff --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.database.VkMessageEntity + +interface MessagesLocalDataSource { + + suspend fun getMessages( + conversationId: Int, + offset: Int?, + count: Int? + ): List + + suspend fun getMessage(messageId: Int): VkMessageEntity? + + suspend fun storeMessages(messages: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt new file mode 100644 index 00000000..c60632f0 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt @@ -0,0 +1,24 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.database.dao.MessageDao +import com.meloda.app.fast.model.database.VkMessageEntity + +// TODO: 05/05/2024, Danil Nikolaev: use paging for room +class MessagesLocalDataSourceImpl( + private val messageDao: MessageDao +) : MessagesLocalDataSource { + + override suspend fun getMessages( + conversationId: Int, + offset: Int?, + count: Int? + ): List = messageDao.getAll(conversationId) + + override suspend fun getMessage( + messageId: Int + ): VkMessageEntity? = messageDao.getById(messageId) + + override suspend fun storeMessages(messages: List) { + messageDao.insertAll(messages) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt new file mode 100644 index 00000000..2ff8faa4 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt @@ -0,0 +1,36 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult + +interface MessagesNetworkDataSource { + + suspend fun getMessagesHistory( + conversationId: Int, + offset: Int?, + count: Int?, + ): ApiResult + + suspend fun getMessageById( + messagesIds: List, + extended: Boolean?, + fields: String? + ): ApiResult + + suspend fun send( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): ApiResult + + suspend fun markAsRead( + peerId: Int, + startMessageId: Int? + ): ApiResult + + suspend fun getMessage(messageId: Int): VkMessage? +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt new file mode 100644 index 00000000..2f009484 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt @@ -0,0 +1,164 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.data.VkGroupsMap +import com.meloda.app.fast.data.VkMemoryCache +import com.meloda.app.fast.data.VkUsersMap +import com.meloda.app.fast.model.api.data.VkContactData +import com.meloda.app.fast.model.api.data.VkGroupData +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.data.asDomain +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.model.api.requests.MessagesGetByIdRequest +import com.meloda.app.fast.model.api.requests.MessagesGetHistoryRequest +import com.meloda.app.fast.model.api.requests.MessagesMarkAsReadRequest +import com.meloda.app.fast.model.api.requests.MessagesSendRequest +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiDefault +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.service.messages.MessagesService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class MessagesNetworkDataSourceImpl( + private val messagesService: MessagesService +) : MessagesNetworkDataSource { + + override suspend fun getMessagesHistory( + conversationId: Int, + offset: Int?, + count: Int? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesGetHistoryRequest( + count = count, + offset = offset, + peerId = conversationId, + extended = true, + startMessageId = null, + rev = null, + fields = VkConstants.ALL_FIELDS + ) + + messagesService.getHistory(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) + val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain) + val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain) + + val usersMap = VkUsersMap.forUsers(profilesList) + val groupsMap = VkGroupsMap.forGroups(groupsList) + + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + val messages = response.items.map { item -> + item.asDomain().let { message -> + message.copy( + user = usersMap.messageUser(message), + group = groupsMap.messageGroup(message), + actionUser = usersMap.messageActionUser(message), + actionGroup = groupsMap.messageActionGroup(message) + ).also { VkMemoryCache[message.id] = it } + } + } + + val conversations = response.conversations.orEmpty().map { item -> + val message = messages.firstOrNull { it.id == item.lastMessageId } + item.asDomain(message) + .let { conversation -> + conversation.copy( + user = usersMap.conversationUser(conversation), + group = groupsMap.conversationGroup(conversation) + ).also { VkMemoryCache[conversation.id] = it } + } + } + + MessagesHistoryDomain( + messages = messages, + conversations = conversations + ) + }, + errorMapper = { error -> + error?.toDomain() + } + ) + } + + override suspend fun getMessageById( + messagesIds: List, + extended: Boolean?, + fields: String? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesGetByIdRequest( + messagesIds = messagesIds, + extended = extended, + fields = fields + ) + + messagesService.getById(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val message = response.items.single() + val usersMap = + VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain)) + val groupsMap = + VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain)) + + message.asDomain().copy( + user = usersMap.messageUser(message), + group = groupsMap.messageGroup(message), + actionUser = usersMap.messageActionUser(message), + actionGroup = groupsMap.messageActionGroup(message) + ) + }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun send( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesSendRequest( + peerId = peerId, + randomId = randomId, + message = message, + replyTo = replyTo, + attachments = attachments + ) + + messagesService.send(requestModel.map).mapApiDefault() + } + + override suspend fun markAsRead( + peerId: Int, + startMessageId: Int? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesMarkAsReadRequest( + peerId = peerId, + startMessageId = startMessageId + ) + + messagesService.markAsRead(requestModel.map).mapApiDefault() + } + + override suspend fun getMessage(messageId: Int): VkMessage? = withContext(Dispatchers.IO) { + // TODO: 05/05/2024, Danil Nikolaev: get message + null + } +} + +data class MessagesHistoryDomain( + val messages: List, + val conversations: List +) diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt new file mode 100644 index 00000000..71b421f8 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt @@ -0,0 +1,75 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.flow.Flow + +interface MessagesRepository { + + suspend fun getMessagesHistory( + conversationId: Int, + offset: Int?, + count: Int? + ): ApiResult + + suspend fun getMessageById( + messagesIds: List, + extended: Boolean?, + fields: String? + ): ApiResult + + suspend fun send( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): ApiResult + + suspend fun markAsRead( + peerId: Int, + startMessageId: Int? + ): ApiResult + + suspend fun getMessage(messageId: Int): Flow + + suspend fun storeMessages(messages: List) + +// suspend fun getHistory( +// params: MessagesGetHistoryRequest +// ): ApiResult + +// suspend fun markAsImportant( +// params: MessagesMarkAsImportantRequest +// ): ApiResult, RestApiErrorDomain> +// +// suspend fun pin( +// params: MessagesPinMessageRequest +// ): ApiResult +// +// suspend fun unpin( +// params: MessagesUnPinMessageRequest +// ): ApiResult +// +// suspend fun delete( +// params: MessagesDeleteRequest +// ): ApiResult +// +// suspend fun edit( +// params: MessagesEditRequest +// ): ApiResult +// +// suspend fun getChat( +// params: MessagesGetChatRequest +// ): ApiResult +// +// suspend fun getConversationMembers( +// params: MessagesGetConversationMembersRequest +// ): ApiResult +// +// suspend fun removeChatUser( +// params: MessagesRemoveChatUserRequest +// ): ApiResult +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt new file mode 100644 index 00000000..4eb00f8d --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt @@ -0,0 +1,201 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.model.api.domain.asEntity +import com.meloda.app.fast.model.database.asExternalModel +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext + +// TODO: 05/05/2024, Danil Nikolaev: implement syncing +class MessagesRepositoryImpl( + private val networkDataSource: MessagesNetworkDataSource, + private val localDataSource: MessagesLocalDataSource +) : MessagesRepository { + + override suspend fun getMessagesHistory( + conversationId: Int, + offset: Int?, + count: Int? + ): ApiResult = withContext(Dispatchers.IO) { +// val localMessages = localDataSource.getMessages( +// conversationId = conversationId, +// offset = offset, +// count = count +// ).map(VkMessageEntity::asExternalModel) +// +// emit(localMessages) +// +// val networkMessages = networkDataSource.getMessagesHistory( +// conversationId = conversationId, +// offset = offset, +// count = count +// ) +// +// emit(networkMessages) + + networkDataSource.getMessagesHistory(conversationId, offset, count) + } + + override suspend fun getMessageById( + messagesIds: List, + extended: Boolean?, + fields: String? + ): ApiResult = withContext(Dispatchers.IO) { + networkDataSource.getMessageById( + messagesIds = messagesIds, + extended = extended, + fields = fields + ) + } + + override suspend fun send( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): ApiResult = withContext(Dispatchers.IO) { + networkDataSource.send( + peerId, + randomId, + message, + replyTo, + attachments + ) + } + + override suspend fun markAsRead( + peerId: Int, + startMessageId: Int? + ): ApiResult = withContext(Dispatchers.IO) { + networkDataSource.markAsRead(peerId, startMessageId) + } + + override suspend fun getMessage(messageId: Int): Flow = flow { + val localMessage = localDataSource.getMessage(messageId)?.asExternalModel() + + emit(localMessage) + + val networkMessage = networkDataSource.getMessage(messageId) + + emit(networkMessage) + } + + override suspend fun storeMessages(messages: List) { + localDataSource.storeMessages(messages.map(VkMessage::asEntity)) + } + + // override suspend fun getHistory( +// params: MessagesGetHistoryRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.getHistory(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun send( +// params: MessagesSendRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.send(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun markAsImportant( +// params: MessagesMarkAsImportantRequest +// ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { +// messagesService.markAsImportant(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun pin( +// params: MessagesPinMessageRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.pin(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun unpin( +// params: MessagesUnPinMessageRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.unpin(params.map).mapResult( +// successMapper = {}, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun delete( +// params: MessagesDeleteRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.delete(params.map).mapResult( +// successMapper = {}, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun edit( +// params: MessagesEditRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.edit(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun getById( +// params: MessagesGetByIdRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.getById(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun markAsRead( +// params: MessagesMarkAsReadRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.markAsRead(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun getChat( +// params: MessagesGetChatRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.getChat(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun getConversationMembers( +// params: MessagesGetConversationMembersRequest +// ): ApiResult = +// withContext(Dispatchers.IO) { +// messagesService.getConversationMembers(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun removeChatUser( +// params: MessagesRemoveChatUserRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.removeChatUser(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt new file mode 100644 index 00000000..14435ebc --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt @@ -0,0 +1,43 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkMessage +import kotlinx.coroutines.flow.Flow + +interface MessagesUseCase { + + fun getMessagesHistory( + conversationId: Int, + count: Int?, + offset: Int? + ): Flow> + + fun getById( + messageId: Int, + extended: Boolean?, + fields: String? + ): Flow> + + fun getByIds( + messageIds: List, + extended: Boolean?, + fields: String? + ): Flow>> + + fun sendMessage( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): Flow> + + fun markAsRead( + peerId: Int, + startMessageId: Int + ): Flow> + + suspend fun storeMessage(message: VkMessage) + suspend fun storeMessages(messages: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt new file mode 100644 index 00000000..bb461775 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt @@ -0,0 +1,15 @@ +package com.meloda.app.fast.data.api.oauth + +import com.meloda.app.fast.model.api.responses.AuthDirectResponse + +interface OAuthRepository { + + suspend fun auth( + login: String, + password: String, + forceSms: Boolean, + twoFaCode: String?, + captchaSid: String?, + captchaKey: String? + ): AuthDirectResponse +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt new file mode 100644 index 00000000..eee89074 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt @@ -0,0 +1,51 @@ +package com.meloda.app.fast.data.api.oauth + +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.model.api.requests.AuthDirectRequest +import com.meloda.app.fast.model.api.responses.AuthDirectResponse +import com.meloda.app.fast.network.service.oauth.OAuthService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class OAuthRepositoryImpl( + private val oAuthService: OAuthService, +) : OAuthRepository { + + override suspend fun auth( + login: String, + password: String, + forceSms: Boolean, + twoFaCode: String?, + captchaSid: String?, + captchaKey: String? + ): AuthDirectResponse = withContext(Dispatchers.IO) { + val requestModel = AuthDirectRequest( + grantType = VkConstants.Auth.GrantType.PASSWORD, + clientId = VkConstants.VK_APP_ID, + clientSecret = VkConstants.VK_SECRET, + username = login, + password = password, + scope = VkConstants.Auth.SCOPE, + twoFaForceSms = forceSms, + twoFaCode = twoFaCode, + captchaSid = captchaSid, + captchaKey = captchaKey, + ) + + when (val result = oAuthService.auth(requestModel.map)) { + is ApiResult.Success -> result.value + + is ApiResult.Failure.HttpFailure -> { + requireNotNull(result.error) + } + + else -> throw IllegalStateException("Unknown result") + +// is ApiResult.Failure.ApiFailure -> TODO() +// is ApiResult.Failure.HttpFailure -> TODO() +// is ApiResult.Failure.NetworkFailure -> TODO() +// is ApiResult.Failure.UnknownFailure -> TODO() + } + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/photos/PhotosRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/photos/PhotosRepository.kt new file mode 100644 index 00000000..02ea7544 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/photos/PhotosRepository.kt @@ -0,0 +1,19 @@ +package com.meloda.app.fast.data.api.photos + +import com.meloda.app.fast.model.api.requests.PhotosSaveMessagePhotoRequest +import com.meloda.app.fast.network.service.photos.PhotosService +import okhttp3.MultipartBody + +class PhotosRepository( + private val photosService: PhotosService +) { + + suspend fun getMessagesUploadServer(peerId: Int) = + photosService.getUploadServer(mapOf("peer_id" to peerId.toString())) + + suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = + photosService.upload(url, photo) + + suspend fun saveMessagePhoto(body: PhotosSaveMessagePhotoRequest) = + photosService.save(body.map) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt new file mode 100644 index 00000000..2430593e --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt @@ -0,0 +1,8 @@ +package com.meloda.app.fast.data.api.users + +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.requests.UsersGetRequest + +interface UsersRepository { + suspend fun getById(params: UsersGetRequest): List +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt new file mode 100644 index 00000000..b6ce26fe --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.data.api.users + +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.requests.UsersGetRequest +import com.meloda.app.fast.network.service.users.UsersService + +class UsersRepositoryImpl( + private val usersService: UsersService +) : UsersRepository { + + override suspend fun getById(params: UsersGetRequest): List { + // TODO: 05/05/2024, Danil Nikolaev: implement + + return emptyList() + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt new file mode 100644 index 00000000..7a47aa06 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt @@ -0,0 +1,23 @@ +package com.meloda.app.fast.data.api.users + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.api.domain.VkUser +import kotlinx.coroutines.flow.Flow + +interface UsersUseCase { + + fun getUserById( + userId: Int, + fields: String?, + nomCase: String? + ): Flow> + + fun getUsersByIds( + userIds: List, + fields: String?, + nomCase: String? + ): Flow>> + + suspend fun storeUser(user: VkUser) + suspend fun storeUsers(users: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt new file mode 100644 index 00000000..ca274362 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt @@ -0,0 +1,67 @@ +package com.meloda.app.fast.data.api.users + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.api.domain.VkUser +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + + +// TODO: 05/05/2024, Danil Nikolaev: implement +class UsersUseCaseImpl( + private val usersRepository: UsersRepository, +) : UsersUseCase { + + override fun getUserById( + userId: Int, + fields: String?, + nomCase: String? + ): Flow> = flow { +// emit(State.Loading) +// +// val newState = usersRepository.getById( +// UsersGetRequest( +// userIds = listOf(userId), +// fields = fields, +// nomCase = nomCase +// ) +// ).fold( +// onSuccess = { response -> State.Success(response.singleOrNull()?.mapToDomain()) }, +// onNetworkFailure = { State.Error.ConnectionError }, +// onUnknownFailure = { State.UNKNOWN_ERROR }, +// onHttpFailure = { result -> result.error.toStateApiError() }, +// onApiFailure = { result -> result.error.toStateApiError() } +// ) +// emit(newState) + } + + override fun getUsersByIds( + userIds: List, + fields: String?, + nomCase: String? + ): Flow>> = flow { +// emit(State.Loading) +// +// val newState = usersRepository.getById( +// UsersGetRequest( +// userIds = userIds, +// fields = fields, +// nomCase = nomCase +// ) +// ).fold( +// onSuccess = { response -> State.Success(response.map(VkUserData::mapToDomain)) }, +// onNetworkFailure = { State.Error.ConnectionError }, +// onUnknownFailure = { State.UNKNOWN_ERROR }, +// onHttpFailure = { result -> result.error.toStateApiError() }, +// onApiFailure = { result -> result.error.toStateApiError() } +// ) +// emit(newState) + } + + override suspend fun storeUser(user: VkUser) { + + } + + override suspend fun storeUsers(users: List) { + + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/videos/VideosRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/videos/VideosRepository.kt new file mode 100644 index 00000000..ecb7b178 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/videos/VideosRepository.kt @@ -0,0 +1,14 @@ +package com.meloda.app.fast.data.api.videos + +import com.meloda.app.fast.network.service.videos.VideosService +import okhttp3.MultipartBody + +class VideosRepository( + private val videosService: VideosService +) { + + suspend fun save() = videosService.save() + + // TODO: 05/05/2024, Danil Nikolaev: research, maybe remove multipart.body + suspend fun upload(url: String, file: MultipartBody.Part) = videosService.upload(url, file) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt new file mode 100644 index 00000000..18074eef --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt @@ -0,0 +1,10 @@ +package com.meloda.app.fast.data.db + +import com.meloda.app.fast.model.database.AccountEntity + +interface AccountsRepository { + + suspend fun getAccounts(): List + + suspend fun storeAccounts(accounts: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt new file mode 100644 index 00000000..81579668 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt @@ -0,0 +1,15 @@ +package com.meloda.app.fast.data.db + +import com.meloda.app.fast.database.dao.AccountDao +import com.meloda.app.fast.model.database.AccountEntity + +class AccountsRepositoryImpl( + private val accountDao: AccountDao +) : AccountsRepository { + + override suspend fun getAccounts(): List = accountDao.getAll() + + override suspend fun storeAccounts( + accounts: List + ) = accountDao.insertAll(accounts) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt new file mode 100644 index 00000000..76d1c4d1 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt @@ -0,0 +1,78 @@ +package com.meloda.app.fast.data.di + +import com.meloda.app.fast.common.di.commonModule +import com.meloda.app.fast.data.api.account.AccountRepository +import com.meloda.app.fast.data.api.account.AccountRepositoryImpl +import com.meloda.app.fast.data.api.account.AccountUseCase +import com.meloda.app.fast.data.api.account.AccountUseCaseImpl +import com.meloda.app.fast.data.api.audios.AudiosRepository +import com.meloda.app.fast.data.api.auth.AuthRepository +import com.meloda.app.fast.data.api.auth.AuthRepositoryImpl +import com.meloda.app.fast.data.api.conversations.ConversationsRepository +import com.meloda.app.fast.data.api.conversations.ConversationsRepositoryImpl +import com.meloda.app.fast.data.api.files.FilesRepository +import com.meloda.app.fast.data.api.friends.FriendsRepository +import com.meloda.app.fast.data.api.friends.FriendsRepositoryImpl +import com.meloda.app.fast.data.api.longpoll.LongPollRepository +import com.meloda.app.fast.data.api.longpoll.LongPollRepositoryImpl +import com.meloda.app.fast.data.api.messages.MessagesLocalDataSource +import com.meloda.app.fast.data.api.messages.MessagesLocalDataSourceImpl +import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSource +import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSourceImpl +import com.meloda.app.fast.data.api.messages.MessagesRepository +import com.meloda.app.fast.data.api.messages.MessagesRepositoryImpl +import com.meloda.app.fast.data.api.oauth.OAuthRepository +import com.meloda.app.fast.data.api.oauth.OAuthRepositoryImpl +import com.meloda.app.fast.data.api.photos.PhotosRepository +import com.meloda.app.fast.data.api.users.UsersRepository +import com.meloda.app.fast.data.api.users.UsersRepositoryImpl +import com.meloda.app.fast.data.api.users.UsersUseCase +import com.meloda.app.fast.data.api.users.UsersUseCaseImpl +import com.meloda.app.fast.data.api.videos.VideosRepository +import com.meloda.app.fast.data.db.AccountsRepository +import com.meloda.app.fast.data.db.AccountsRepositoryImpl +import com.meloda.app.fast.database.di.databaseModule +import com.meloda.app.fast.datastore.di.dataStoreModule +import com.meloda.app.fast.network.di.networkModule +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +val dataModule = module { + includes( + commonModule, + databaseModule, + dataStoreModule, + networkModule, + ) + + singleOf(::AccountRepositoryImpl) bind AccountRepository::class + singleOf(::AccountUseCaseImpl) bind AccountUseCase::class + + singleOf(::AudiosRepository) + + singleOf(::AuthRepositoryImpl) bind AuthRepository::class + + singleOf(::ConversationsRepositoryImpl) bind ConversationsRepository::class + + singleOf(::FilesRepository) + + singleOf(::LongPollRepositoryImpl) bind LongPollRepository::class + + singleOf(::MessagesLocalDataSourceImpl) bind MessagesLocalDataSource::class + singleOf(::MessagesNetworkDataSourceImpl) bind MessagesNetworkDataSource::class + singleOf(::MessagesRepositoryImpl) bind MessagesRepository::class + + singleOf(::OAuthRepositoryImpl) bind OAuthRepository::class + + singleOf(::PhotosRepository) + + singleOf(::UsersRepositoryImpl) bind UsersRepository::class + singleOf(::UsersUseCaseImpl) bind UsersUseCase::class + + singleOf(::VideosRepository) + + singleOf(::AccountsRepositoryImpl) bind AccountsRepository::class + + singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class +} diff --git a/core/database/.gitignore b/core/database/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/database/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts new file mode 100644 index 00000000..bc87cf96 --- /dev/null +++ b/core/database/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.com.google.devtools.ksp) +} + +group = "com.meloda.app.fast.database" + +android { + namespace = "com.meloda.app.fast.database" + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + arg("room.generateKotlin", "true") + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + } +} + +dependencies { + api(projects.core.model) + + implementation(libs.room.ktx) + implementation(libs.room.runtime) + ksp(libs.room.compiler) + + implementation(libs.koin.android) +} diff --git a/core/database/src/main/AndroidManifest.xml b/core/database/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/database/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/AccountsDatabase.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/AccountsDatabase.kt new file mode 100644 index 00000000..ec7a0a67 --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/AccountsDatabase.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.meloda.app.fast.database.dao.AccountDao +import com.meloda.app.fast.model.database.AccountEntity + +@Database( + entities = [AccountEntity::class], + version = 2 +) +abstract class AccountsDatabase : RoomDatabase() { + abstract fun accountDao(): AccountDao +} + + diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt new file mode 100644 index 00000000..35521021 --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt @@ -0,0 +1,32 @@ +package com.meloda.app.fast.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.meloda.app.fast.database.dao.ConversationDao +import com.meloda.app.fast.database.dao.GroupDao +import com.meloda.app.fast.database.dao.MessageDao +import com.meloda.app.fast.database.dao.UsersDao +import com.meloda.app.fast.database.typeconverters.Converters +import com.meloda.app.fast.model.database.VkConversationEntity +import com.meloda.app.fast.model.database.VkGroupEntity +import com.meloda.app.fast.model.database.VkMessageEntity +import com.meloda.app.fast.model.database.VkUserEntity + +@Database( + entities = [ + VkUserEntity::class, + VkGroupEntity::class, + VkMessageEntity::class, + VkConversationEntity::class + ], + + version = 5 +) +@TypeConverters(Converters::class) +abstract class CacheDatabase : RoomDatabase() { + abstract fun userDao(): UsersDao + abstract fun groupDao(): GroupDao + abstract fun messageDao(): MessageDao + abstract fun conversationDao(): ConversationDao +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt new file mode 100644 index 00000000..c960accc --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt @@ -0,0 +1,15 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.meloda.app.fast.model.database.AccountEntity + +@Dao +abstract class AccountDao : EntityDao { + + @Query("SELECT * FROM accounts") + abstract suspend fun getAll(): List + + @Query("DELETE FROM accounts WHERE userId = :userId") + abstract suspend fun deleteById(userId: Int) +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/ConversationDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/ConversationDao.kt new file mode 100644 index 00000000..1124c8bd --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/ConversationDao.kt @@ -0,0 +1,30 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import com.meloda.app.fast.model.database.ConversationWithMessage +import com.meloda.app.fast.model.database.VkConversationEntity + +@Dao +abstract class ConversationDao : EntityDao { + + @Query("SELECT * FROM conversations") + abstract suspend fun getAll(): List + + @Query("SELECT * FROM conversations WHERE id IN (:ids)") + abstract suspend fun getAllByIds(ids: List): List + + @Query("SELECT * FROM conversations WHERE id IS (:id)") + abstract suspend fun getById(id: Int): VkConversationEntity? + + @Transaction + @Query("SELECT * FROM conversations WHERE id IS (:id)") + abstract suspend fun getByIdWithMessage(id: Int): ConversationWithMessage? + + @Query("DELETE FROM conversations WHERE rowid IN (:ids)") + abstract suspend fun deleteByIds(ids: List): Int +} + + + diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/EntityDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/EntityDao.kt new file mode 100644 index 00000000..a6ab07a0 --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/EntityDao.kt @@ -0,0 +1,20 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy + +interface EntityDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(values: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(value: T) + + @Delete + suspend fun delete(value: T): Int + + @Delete + suspend fun deleteAll(values: List): Int +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/GroupDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/GroupDao.kt new file mode 100644 index 00000000..1fabe1ce --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/GroupDao.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.meloda.app.fast.model.database.VkGroupEntity + +@Dao +abstract class GroupDao : EntityDao { + + @Query("SELECT * FROM groups") + abstract suspend fun getAll(): List + + @Query("SELECT * FROM groups WHERE id IN (:ids)") + abstract suspend fun getAllByIds(ids: List): List + + @Query("DELETE FROM groups WHERE id IN (:ids)") + abstract suspend fun deleteByIds(ids: List): Int +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/MessageDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/MessageDao.kt new file mode 100644 index 00000000..597c406f --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/MessageDao.kt @@ -0,0 +1,24 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.meloda.app.fast.model.database.VkMessageEntity + +@Dao +abstract class MessageDao : EntityDao { + + @Query("SELECT * FROM messages") + abstract suspend fun getAll(): List + + @Query("SELECT * FROM messages WHERE peerId IS (:conversationId)") + abstract suspend fun getAll(conversationId: Int): List + + @Query("SELECT * FROM messages WHERE id IN (:ids)") + abstract suspend fun getAllByIds(ids: List): List + + @Query("SELECT * FROM messages WHERE id IS (:messageId)") + abstract suspend fun getById(messageId: Int): VkMessageEntity? + + @Query("DELETE FROM messages WHERE id IN (:ids)") + abstract suspend fun deleteByIds(ids: List): Int +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/UsersDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/UsersDao.kt new file mode 100644 index 00000000..a9cd10f5 --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/UsersDao.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.meloda.app.fast.model.database.VkUserEntity + +@Dao +abstract class UsersDao : EntityDao { + + @Query("SELECT * FROM users") + abstract suspend fun getAll(): List + + @Query("SELECT * FROM users WHERE id IN (:ids)") + abstract suspend fun getAllByIds(ids: List): List + + @Query("DELETE FROM users WHERE id IN (:ids)") + abstract suspend fun deleteByIds(ids: List): Int +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/di/DatabaseModule.kt new file mode 100644 index 00000000..9cbd0a8a --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/di/DatabaseModule.kt @@ -0,0 +1,25 @@ +package com.meloda.app.fast.database.di + +import androidx.room.Room +import com.meloda.app.fast.database.AccountsDatabase +import org.koin.core.scope.Scope +import org.koin.dsl.module + +val databaseModule = module { + single { + Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts").build() + } + single { get().accountDao() } + + single { + Room.databaseBuilder(get(), com.meloda.app.fast.database.CacheDatabase::class.java, "cache") + .fallbackToDestructiveMigration() + .build() + } + single { cacheDB().userDao() } + single { cacheDB().groupDao() } + single { cacheDB().messageDao() } + single { cacheDB().conversationDao() } +} + +private fun Scope.cacheDB(): com.meloda.app.fast.database.CacheDatabase = get() diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/typeconverters/Converters.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/typeconverters/Converters.kt new file mode 100644 index 00000000..791c769e --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/typeconverters/Converters.kt @@ -0,0 +1,21 @@ +package com.meloda.app.fast.database.typeconverters + +import androidx.room.TypeConverter + +class Converters { + + @TypeConverter + fun intListToString(list: List): String = list.joinToString() + + @TypeConverter + fun stringToIntList(string: String): List = + string + .split(", ") + .mapNotNull(String::toIntOrNull) + + @TypeConverter + fun stringListToString(list: List): String = list.joinToString() + + @TypeConverter + fun stringToStringList(string: String): List = string.split(", ") +} diff --git a/core/datastore/.gitignore b/core/datastore/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/datastore/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts new file mode 100644 index 00000000..d85cce98 --- /dev/null +++ b/core/datastore/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) +} + +group = "com.meloda.app.fast.datastore" + +android { + namespace = "com.meloda.app.fast.datastore" + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + } +} + +dependencies { + api(projects.core.common) + + implementation(libs.koin.android) +} diff --git a/core/datastore/src/main/AndroidManifest.xml b/core/datastore/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/datastore/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/Extensions.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/Extensions.kt new file mode 100644 index 00000000..b070ebc5 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/Extensions.kt @@ -0,0 +1,57 @@ +package com.meloda.app.fast.datastore + +import android.content.res.Configuration +import android.content.res.Resources +import android.os.PowerManager +import androidx.appcompat.app.AppCompatDelegate + +fun isUsingDarkMode( + resources: Resources, + powerManager: PowerManager, +): Boolean { + val nightThemeMode: Int = SettingsController.getInt( + SettingsKeys.KEY_APPEARANCE_DARK_THEME, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME + ) + + val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES + val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY + + val systemUiNightMode = resources.configuration.uiMode + + val isSystemBatterySaver = powerManager.isPowerSaveMode + val isSystemUsingDarkTheme = + systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + + return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) +} + +fun isUsingDynamicColors(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_USE_DYNAMIC_COLORS, + SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS +) + +fun isUsingAmoledBackground(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_APPEARANCE_AMOLED_THEME, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_AMOLED_THEME +) + +fun selectedColorScheme(): Int = SettingsController.getInt( + SettingsKeys.KEY_APPEARANCE_COLOR_SCHEME, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME +) + +fun isUsingBlur(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_APPEARANCE_BLUR, + SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_BLUR +) + +fun isDebugSettingsShown(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, + false +) + +fun isMultiline(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_APPEARANCE_MULTILINE, + SettingsKeys.DEFAULT_VALUE_MULTILINE +) diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt new file mode 100644 index 00000000..8fc99016 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt @@ -0,0 +1,53 @@ +package com.meloda.app.fast.datastore + +import android.content.SharedPreferences +import androidx.core.content.edit +import kotlin.properties.Delegates + +object SettingsController { + + private var preferences: SharedPreferences by Delegates.notNull() + + fun init(preferences: SharedPreferences) { + this.preferences = preferences + } + + fun edit( + commit: Boolean = false, + action: SharedPreferences.Editor.() -> Unit + ) { + preferences.edit(commit, action) + } + + fun getString(key: String, defaultValue: String?): String? { + return preferences.getString(key, defaultValue) + } + + fun getBoolean(key: String, defaultValue: Boolean): Boolean { + return preferences.getBoolean(key, defaultValue) + } + + fun getInt(key: String, defaultValue: Int): Int { + return preferences.getInt(key, defaultValue) + } + + fun getLong(key: String, defaultValue: Long): Long { + return preferences.getLong(key, defaultValue) + } + + fun getFloat(key: String, defaultValue: Float): Float { + return preferences.getFloat(key, defaultValue) + } + + fun put(key: String, newValue: T?) { + preferences.edit { + when (newValue) { + is String -> putString(key, newValue) + is Boolean -> putBoolean(key, newValue) + is Int -> putInt(key, newValue) + is Long -> putLong(key, newValue) + is Float -> putFloat(key, newValue) + } + } + } +} diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt new file mode 100644 index 00000000..1f11ab5b --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt @@ -0,0 +1,50 @@ +package com.meloda.app.fast.datastore + +import androidx.appcompat.app.AppCompatDelegate + +object SettingsKeys { + const val KEY_ACCOUNT = "account" + const val KEY_ACCOUNT_LOGOUT = "account_logout" + + const val KEY_GENERAL = "general" + const val KEY_USE_CONTACT_NAMES = "general_use_contact_names" + const val DEFAULT_VALUE_USE_CONTACT_NAMES = false + const val KEY_SHOW_EMOJI_BUTTON = "general_show_emoji_button" + const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false + + const val KEY_APPEARANCE = "appearance" + const val KEY_APPEARANCE_MULTILINE = "appearance_multiline" + const val DEFAULT_VALUE_MULTILINE = true + const val KEY_APPEARANCE_DARK_THEME = "appearance_appearance_dark_theme" + const val DEFAULT_VALUE_APPEARANCE_DARK_THEME = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + const val KEY_APPEARANCE_AMOLED_THEME = "appearance_amoled_theme" + const val DEFAULT_VALUE_APPEARANCE_AMOLED_THEME = false + const val KEY_USE_DYNAMIC_COLORS = "appearance_use_dynamic_colors" + const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false + const val KEY_APPEARANCE_COLOR_SCHEME = "appearance_color_scheme" + const val DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME = 0 + const val KEY_APPEARANCE_LANGUAGE = "appearance_language" + const val KEY_APPEARANCE_BLUR = "appearance_blur" + const val DEFAULT_VALUE_KEY_APPEARANCE_BLUR = false + + const val KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL = "features_hide_keyboard_on_scroll" + const val KEY_FEATURES_FAST_TEXT = "features_fast_text" + const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯" + const val KEY_FEATURES_LONG_POLL_IN_BACKGROUND = "features_lp_background" + const val DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND = false + + const val KEY_VISIBILITY_SEND_ONLINE_STATUS = "visibility_send_online_status" + const val DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS = false + + const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash" + const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" + const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list" + const val KEY_SHOW_EXACT_TIME_ON_TIME_STAMP = "wip_show_exact_time_on_time_stamp" + const val KEY_SHOW_NAME_IN_BUBBLES = "debug_show_title_in_bubbles" + const val KEY_SHOW_DATE_UNDER_BUBBLES = "debug_show_date_under_bubbles" + const val KEY_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages" + + const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" + + const val ID_DMITRY = 37610580 +} diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt new file mode 100644 index 00000000..c5ff9f9d --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt @@ -0,0 +1,120 @@ +package com.meloda.app.fast.datastore + +import android.content.res.Resources +import android.os.PowerManager +import com.meloda.app.fast.datastore.model.ThemeConfig +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +interface UserSettings { + val theme: StateFlow + val longPollBackground: StateFlow + val online: StateFlow + val debugSettingsEnabled: StateFlow + val useContactNames: StateFlow + + fun updateUsingDarkTheme() + fun useDarkThemeChanged(use: Boolean) + fun useAmoledThemeChanged(use: Boolean) + fun useDynamicColorsChanged(use: Boolean) + fun useBlurChanged(use: Boolean) + fun useMultiline(use: Boolean) + fun setLongPollBackground(background: Boolean) + fun setOnline(use: Boolean) + fun enableDebugSettings(enable: Boolean) + fun onUseContactNamesChanged(use: Boolean) +} + +class UserSettingsImpl( + private val resources: Resources, + private val powerManager: PowerManager +) : UserSettings { + + override val theme = MutableStateFlow( + ThemeConfig( + usingDarkStyle = isUsingDarkMode(resources, powerManager), + usingDynamicColors = isUsingDynamicColors(), + selectedColorScheme = selectedColorScheme(), + usingAmoledBackground = isUsingAmoledBackground(), + usingBlur = isUsingBlur(), + multiline = isMultiline() + ) + ) + + override val longPollBackground = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND + ) + ) + override val online = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS, + SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS + ) + ) + + override val debugSettingsEnabled = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, + false + ) + ) + + override val useContactNames = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_USE_CONTACT_NAMES, + SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES + ) + ) + + override fun updateUsingDarkTheme() { + useDarkThemeChanged( + isUsingDarkMode( + resources = resources, + powerManager = powerManager, + ) + ) + } + + override fun useDarkThemeChanged(use: Boolean) { + theme.value = theme.value.copy( + usingDarkStyle = use + ) + } + + override fun useAmoledThemeChanged(use: Boolean) { + theme.value = theme.value.copy( + usingAmoledBackground = use + ) + } + + override fun useDynamicColorsChanged(use: Boolean) { + theme.value = theme.value.copy(usingDynamicColors = use) + } + + override fun useBlurChanged(use: Boolean) { + theme.value = theme.value.copy(usingBlur = use) + } + + override fun useMultiline(use: Boolean) { + theme.value = theme.value.copy(multiline = use) + } + + override fun setLongPollBackground(background: Boolean) { + longPollBackground.value = background + } + + override fun setOnline(use: Boolean) { + online.value = use + } + + override fun enableDebugSettings(enable: Boolean) { + debugSettingsEnabled.update { enable } + } + + override fun onUseContactNamesChanged(use: Boolean) { + useContactNames.update { use } + } +} diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/di/DataStoreModule.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/di/DataStoreModule.kt new file mode 100644 index 00000000..7a595007 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/di/DataStoreModule.kt @@ -0,0 +1,11 @@ +package com.meloda.app.fast.datastore.di + +import com.meloda.app.fast.datastore.UserSettings +import com.meloda.app.fast.datastore.UserSettingsImpl +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +val dataStoreModule = module { + singleOf(::UserSettingsImpl) bind UserSettings::class +} diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt new file mode 100644 index 00000000..cc94c419 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt @@ -0,0 +1,11 @@ +package com.meloda.app.fast.datastore.model + +data class ThemeConfig( + val usingDarkStyle: Boolean, + val usingDynamicColors: Boolean, + val selectedColorScheme: Int, + val usingAmoledBackground: Boolean, + val usingBlur: Boolean, + val multiline: Boolean, + val bubblesWithPinch: Boolean = true +) diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/designsystem/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 00000000..dc6d4af4 --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.kotlin.compose.compiler) +} + +group = "com.meloda.app.fast.designsystem" + +android { + namespace = "com.meloda.app.fast.designsystem" + + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + } + buildFeatures { + compose = true + } + composeOptions { + useLiveLiterals = true + } +} + +dependencies { + // TODO: 05/05/2024, Danil Nikolaev: maybe remove + implementation(projects.core.common) + implementation(projects.core.datastore) + + implementation(libs.appcompat) + implementation(libs.accompanist.permissions) + + implementation(platform(libs.compose.bom)) + implementation(libs.bundles.compose) + + implementation(libs.haze) + implementation(libs.haze.materials) +} diff --git a/core/designsystem/src/main/AndroidManifest.xml b/core/designsystem/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/designsystem/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt new file mode 100644 index 00000000..7ec28bcc --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt @@ -0,0 +1,178 @@ +package com.meloda.app.fast.designsystem + +import android.app.Activity +import android.os.Build +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.core.view.WindowCompat +import com.meloda.app.fast.datastore.isUsingAmoledBackground +import com.meloda.app.fast.datastore.isUsingDynamicColors +import com.meloda.app.fast.datastore.model.ThemeConfig +import com.meloda.app.fast.datastore.selectedColorScheme +import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme + +private val googleSansFonts = FontFamily( + Font(resId = R.font.google_sans_regular), + Font( + resId = R.font.google_sans_italic, + style = FontStyle.Italic + ), + Font( + resId = R.font.google_sans_medium, + weight = FontWeight.Medium + ), + Font( + resId = R.font.google_sans_medium_italic, + weight = FontWeight.Medium, + style = FontStyle.Italic + ), + Font( + resId = R.font.google_sans_bold, + weight = FontWeight.Bold + ), + Font( + resId = R.font.google_sans_bold_italic, + weight = FontWeight.Bold, + style = FontStyle.Italic + ) +) + +private val robotoFonts = FontFamily( + Font( + resId = R.font.roboto_thin, + weight = FontWeight.Thin + ), + Font( + resId = R.font.roboto_thin_italic, + weight = FontWeight.Thin, + style = FontStyle.Italic + ), + Font( + resId = R.font.roboto_light, + weight = FontWeight.Light + ), + Font( + resId = R.font.roboto_light_italic, + weight = FontWeight.Light, + style = FontStyle.Italic + ), + Font(resId = R.font.roboto_regular), + Font( + resId = R.font.roboto_italic, + style = FontStyle.Italic + ), + Font( + resId = R.font.roboto_medium, + weight = FontWeight.Medium + ), + Font( + resId = R.font.roboto_medium_italic, + weight = FontWeight.Medium, + style = FontStyle.Italic + ), + Font( + resId = R.font.roboto_bold, + weight = FontWeight.Bold + ), + Font( + resId = R.font.roboto_bold_italic, + weight = FontWeight.Bold, + style = FontStyle.Italic + ), + Font( + resId = R.font.roboto_black, + weight = FontWeight.Black + ), + Font( + resId = R.font.roboto_black_italic, + weight = FontWeight.Black, + style = FontStyle.Italic + ) +) + +val LocalTheme = compositionLocalOf { + ThemeConfig( + usingDarkStyle = false, + usingDynamicColors = false, + selectedColorScheme = 0, + usingAmoledBackground = false, + usingBlur = false, + multiline = false + ) +} + +@Composable +fun AppTheme( + predefinedColorScheme: ColorScheme? = null, + useDarkTheme: Boolean = isUsingDarkTheme(), + useDynamicColors: Boolean = isUsingDynamicColors(), + selectedColorScheme: Int = selectedColorScheme(), + useAmoledBackground: Boolean = isUsingAmoledBackground(), + content: @Composable () -> Unit +) { + val colorScheme: ColorScheme = when { + useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (useDarkTheme) dynamicDarkColorScheme(context) + else dynamicLightColorScheme(context) + } + + else -> { + // TODO: 03/07/2024, Danil Nikolaev: add color picker to settings + when (selectedColorScheme) { + 1 -> if (useDarkTheme) darkColorScheme() else lightColorScheme() + else -> if (useDarkTheme) ClassicColorScheme.darkScheme else ClassicColorScheme.lightScheme + } + } + }.let { scheme -> + if (useDarkTheme && useAmoledBackground) { + scheme.copy( + background = Color.Black, + surface = Color.Black + ) + } else { + scheme + } + } + + val typography = MaterialTheme.typography.copy( + displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = googleSansFonts), + displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = googleSansFonts), + displaySmall = MaterialTheme.typography.displaySmall.copy(fontFamily = googleSansFonts), + headlineLarge = MaterialTheme.typography.headlineLarge.copy(fontFamily = googleSansFonts), + headlineMedium = MaterialTheme.typography.headlineMedium.copy(fontFamily = googleSansFonts), + headlineSmall = MaterialTheme.typography.headlineSmall.copy(fontFamily = googleSansFonts), + bodyLarge = MaterialTheme.typography.bodyLarge.copy(fontFamily = robotoFonts), + bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontFamily = robotoFonts), + bodySmall = MaterialTheme.typography.bodySmall.copy(fontFamily = robotoFonts) + ) + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + val insetsController = WindowCompat.getInsetsController(window, view) + insetsController.isAppearanceLightStatusBars = !useDarkTheme + } + } + + MaterialTheme( + colorScheme = predefinedColorScheme ?: colorScheme, + typography = typography, + content = content + ) +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AutoFill.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AutoFill.kt new file mode 100644 index 00000000..7aedae57 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AutoFill.kt @@ -0,0 +1,121 @@ +package com.meloda.app.fast.designsystem + +import android.os.Build +import android.view.autofill.AutofillManager +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.Autofill +import androidx.compose.ui.autofill.AutofillNode +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalAutofill +import androidx.compose.ui.platform.LocalAutofillTree +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import kotlin.math.roundToInt + +fun Modifier.connectNode(handler: AutoFillHandler): Modifier { + return with(handler) { fillBounds() } +} + +fun Modifier.defaultFocusChangeAutoFill(handler: AutoFillHandler): Modifier { + return this.then( + Modifier.onFocusChanged { + if (it.isFocused) { + handler.request() + } else { + handler.cancel() + } + } + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun autoFillRequestHandler( + autofillTypes: List = listOf(), + onFill: (String) -> Unit, +): AutoFillHandler { + val view = LocalView.current + val context = LocalContext.current + var isFillRecently = remember { false } + val autoFillNode = remember { + AutofillNode( + autofillTypes = autofillTypes, + onFill = { + isFillRecently = true + onFill(it) + } + ) + } + val autofill = LocalAutofill.current + LocalAutofillTree.current += autoFillNode + return remember { + @RequiresApi(Build.VERSION_CODES.O) + object : AutoFillHandler { + val autofillManager = context.getSystemService(AutofillManager::class.java) + override fun requestManual() { + autofillManager.requestAutofill( + view, + autoFillNode.id, + autoFillNode.boundingBox?.toAndroidRect() ?: error("BoundingBox is not provided yet") + ) + } + + override fun requestVerifyManual() { + if (isFillRecently) { + isFillRecently = false + requestManual() + } + } + + override val autoFill: Autofill? + get() = autofill + + override val autoFillNode: AutofillNode + get() = autoFillNode + + override fun request() { + autofill?.requestAutofillForNode(autofillNode = autoFillNode) + } + + override fun cancel() { + autofill?.cancelAutofillForNode(autofillNode = autoFillNode) + } + + override fun Modifier.fillBounds(): Modifier { + return this.then( + Modifier.onGloballyPositioned { + autoFillNode.boundingBox = it.boundsInWindow() + }) + } + } + } +} + +fun Rect.toAndroidRect(): android.graphics.Rect { + return android.graphics.Rect( + left.roundToInt(), + top.roundToInt(), + right.roundToInt(), + bottom.roundToInt() + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +interface AutoFillHandler { + + val autoFill: Autofill? + val autoFillNode: AutofillNode + fun requestVerifyManual() + fun requestManual() + fun request() + fun cancel() + fun Modifier.fillBounds(): Modifier +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ContentAlpha.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ContentAlpha.kt new file mode 100644 index 00000000..ef32e974 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ContentAlpha.kt @@ -0,0 +1,133 @@ +package com.meloda.app.fast.designsystem + +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.annotation.FloatRange +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.luminance + +/** + * Default alpha levels used by Material components. + * + * See [LocalContentAlpha]. + */ +object ContentAlpha { + /** + * A high level of content alpha, used to represent high emphasis text such as input text in a + * selected [TextField]. + */ + val high: Float + @Composable + get() = contentAlpha( + highContrastAlpha = HighContrastContentAlpha.high, + lowContrastAlpha = LowContrastContentAlpha.high + ) + + /** + * A medium level of content alpha, used to represent medium emphasis text such as + * placeholder text in a [TextField]. + */ + val medium: Float + @Composable + get() = contentAlpha( + highContrastAlpha = HighContrastContentAlpha.medium, + lowContrastAlpha = LowContrastContentAlpha.medium + ) + + /** + * A low level of content alpha used to represent disabled components, such as text in a + * disabled [Button]. + */ + val disabled: Float + @Composable + get() = contentAlpha( + highContrastAlpha = HighContrastContentAlpha.disabled, + lowContrastAlpha = LowContrastContentAlpha.disabled + ) + + /** + * This default implementation uses separate alpha levels depending on the luminance of the + * incoming color, and whether the theme is light or dark. This is to ensure correct contrast + * and accessibility on all surfaces. + * + * See [HighContrastContentAlpha] and [LowContrastContentAlpha] for what the levels are + * used for, and under what circumstances. + */ + @Composable + private fun contentAlpha( + @FloatRange(from = 0.0, to = 1.0) + highContrastAlpha: Float, + @FloatRange(from = 0.0, to = 1.0) + lowContrastAlpha: Float + ): Float { + val contentColor = LocalContentColor.current + return if (!isUsingDarkTheme()) { + if (contentColor.luminance() > 0.5) highContrastAlpha else lowContrastAlpha + } else { + if (contentColor.luminance() < 0.5) highContrastAlpha else lowContrastAlpha + } + } +} + +/** + * CompositionLocal containing the preferred content alpha for a given position in the hierarchy. + * This alpha is used for text and iconography ([Text] and [Icon]) to emphasize / de-emphasize + * different parts of a component. See the Material guide on + * [Text Legibility](https://material.io/design/color/text-legibility.html) for more information on + * alpha levels used by text and iconography. + * + * See [ContentAlpha] for the default levels used by most Material components. + * + * [MaterialTheme] sets this to [ContentAlpha.high] by default, as this is the default alpha for + * body text. + * + * @sample androidx.compose.material.samples.ContentAlphaSample + */ +val LocalContentAlpha = compositionLocalOf { 1f } + +/** + * Alpha levels for high luminance content in light theme, or low luminance content in dark theme. + * + * This content will typically be placed on colored surfaces, so it is important that the + * contrast here is higher to meet accessibility standards, and increase legibility. + * + * These levels are typically used for text / iconography in primary colored tabs / + * bottom navigation / etc. + */ +private object HighContrastContentAlpha { + const val high: Float = 1.00f + const val medium: Float = 0.74f + const val disabled: Float = 0.38f +} + +/** + * Alpha levels for low luminance content in light theme, or high luminance content in dark theme. + * + * This content will typically be placed on grayscale surfaces, so the contrast here can be lower + * without sacrificing accessibility and legibility. + * + * These levels are typically used for body text on the main surface (white in light theme, grey + * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc. + */ +private object LowContrastContentAlpha { + const val high: Float = 0.87f + const val medium: Float = 0.60f + const val disabled: Float = 0.38f +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt new file mode 100644 index 00000000..4e8fd568 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt @@ -0,0 +1,112 @@ +package com.meloda.app.fast.designsystem + +import android.content.res.Configuration +import android.view.KeyEvent +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.PermissionStatus +import com.meloda.app.fast.common.UiText +import com.meloda.app.fast.common.util.AndroidUtils +import com.meloda.app.fast.datastore.SettingsController +import com.meloda.app.fast.datastore.SettingsKeys + +@Composable +fun isUsingDarkTheme(): Boolean { + val nightThemeMode = SettingsController.getInt( + SettingsKeys.KEY_APPEARANCE_DARK_THEME, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME + ) + val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES + val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY + + val context = LocalContext.current + + val systemUiNightMode = context.resources.configuration.uiMode + + val isSystemBatterySaver = AndroidUtils.isBatterySaverOn(context) + val isSystemUsingDarkTheme = + systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + + return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) +} + +@Composable +fun UiText?.getString(): String? { + return when (this) { + is UiText.Resource -> { + stringResource(id = resId) + } + + is UiText.ResourceParams -> { + val processedArgs = args.map { any -> + when (any) { + is UiText -> any.getString().orEmpty() + else -> any.toString() + } + }.toTypedArray() + + stringResource(id = value, *processedArgs) + } + + is UiText.QuantityResource -> { + pluralStringResource(id = resId, count = quantity, quantity) + } + + is UiText.Simple -> text + + else -> null + } +} + +fun Modifier.handleTabKey( + action: () -> Boolean +): Modifier = this.onKeyEvent { event -> + if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB) { + action.invoke() + } else false +} + +fun Modifier.handleEnterKey( + action: () -> Boolean +): Modifier = this.onKeyEvent { event -> + if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { + action.invoke() + } else false +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun CheckPermission( + showRationale: @Composable () -> Unit, + onDenied: @Composable () -> Unit, + permission: PermissionState, +) { + when (val status = permission.status) { + is PermissionStatus.Denied -> { + if (status.shouldShowRationale) { + showRationale() + } else { + onDenied() + } + } + + is PermissionStatus.Granted -> Unit + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun RequestPermission( + permission: PermissionState +) { + LaunchedEffect(Unit) { permission.launchPermissionRequest() } +} + diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt new file mode 100644 index 00000000..7ea0e6fb --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt @@ -0,0 +1,59 @@ +package com.meloda.app.fast.designsystem + +import androidx.compose.runtime.Immutable + +@Immutable +class ImmutableList(val values: List) : Iterable { + + constructor(size: Int, init: (index: Int) -> T) : this(MutableList(size, init)) + + operator fun get(index: Int): T? { + values.singleOrNull() + return values[index] + } + + inline fun forEach(action: (T) -> Unit) { + for (element in values) action(element) + } + + inline fun map(transform: (T) -> R): ImmutableList { + return values.map(transform).toImmutableList() + } + + inline fun mapIndexed(transform: (index: Int, T) -> R): ImmutableList { + return values.mapIndexed(transform).toImmutableList() + } + + fun singleOrNull(): T? { + return if (values.size == 1) this[0] else null + } + + fun isEmpty(): Boolean = values.isEmpty() + + fun isNotEmpty(): Boolean = !isEmpty() + + inline fun singleOrNull(predicate: (T) -> Boolean): T? { + var single: T? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) return null + single = element + found = true + } + } + if (!found) return null + return single + } + + companion object { + fun copyOf(collection: Collection): ImmutableList = + ImmutableList(collection.toList()) + + fun List.toImmutableList(): ImmutableList = ImmutableList(this) + + fun empty(): ImmutableList = ImmutableList(emptyList()) + } + + override fun iterator(): Iterator = values.listIterator() +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/LocalContentAlpha.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/LocalContentAlpha.kt new file mode 100644 index 00000000..45671625 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/LocalContentAlpha.kt @@ -0,0 +1,20 @@ +package com.meloda.app.fast.designsystem + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color + +@Composable +fun LocalContentAlpha( + defaultColor: Color = MaterialTheme.colorScheme.onBackground, + alpha: Float, + content: @Composable () -> Unit +) { + CompositionLocalProvider( + LocalContentColor provides defaultColor.copy(alpha = alpha) + ) { + content() + } +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt new file mode 100644 index 00000000..853b3545 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt @@ -0,0 +1,346 @@ +package com.meloda.app.fast.designsystem + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.meloda.app.fast.common.UiText +import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList + +// TODO: 08.04.2023, Danil Nikolaev: review +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MaterialDialog( + onDismissAction: (() -> Unit), + title: UiText? = null, + text: UiText? = null, + confirmText: UiText? = null, + confirmAction: (() -> Unit)? = null, + cancelText: UiText? = null, + cancelAction: (() -> Unit)? = null, + neutralText: UiText? = null, + neutralAction: (() -> Unit)? = null, + itemsSelectionType: ItemsSelectionType = ItemsSelectionType.None, + preSelectedItems: ImmutableList = ImmutableList.empty(), + items: ImmutableList = ImmutableList.empty(), + onItemClick: ((index: Int) -> Unit)? = null, + buttonsInvokeDismiss: Boolean = true, + customContent: (@Composable ColumnScope.() -> Unit)? = null, +) { + var isVisible by remember { + mutableStateOf(true) + } + val onDismissRequest = { + onDismissAction.invoke() + isVisible = false + } + + val stringTitles = items.map { it.getString().orEmpty() } + + var alertItems by remember { + mutableStateOf( + stringTitles.mapIndexed { index, title -> + DialogItem( + title, + preSelectedItems.contains(index) + ) + } + ) + } + + AppTheme { + if (isVisible) { +// AlertAnimation(visible = isVisible) { + BasicAlertDialog( + onDismissRequest = onDismissRequest + ) { + val scrollState = rememberScrollState() + val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } } + val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } } + + Surface( + modifier = Modifier.fillMaxWidth(), + color = AlertDialogDefaults.containerColor, + shape = AlertDialogDefaults.shape, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column(modifier = Modifier.padding(bottom = 10.dp)) { + val stringTitle = title?.getString() + if (stringTitle != null) { + Spacer(modifier = Modifier.height(20.dp)) + } + + Row { + stringTitle?.let { title -> + Spacer(modifier = Modifier.width(24.dp)) + Text( + modifier = Modifier.weight(1f), + text = title, + style = MaterialTheme.typography.headlineSmall + ) + Spacer(modifier = Modifier.width(20.dp)) + } + } + + if (canScrollBackward) { + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false) + .verticalScroll(scrollState) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + val stringMessage = text?.getString() + if (stringMessage != null && stringTitle == null) { + Spacer(modifier = Modifier.height(20.dp)) + } + + Row { + stringMessage?.let { message -> + Spacer(modifier = Modifier.width(24.dp)) + Text( + modifier = Modifier.weight(1f), + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(20.dp)) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + if (alertItems.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + AlertItems( + selectionType = itemsSelectionType, + items = alertItems, + onItemClick = { index -> + onItemClick?.invoke(index) + + if (itemsSelectionType == ItemsSelectionType.None) { + onDismissRequest.invoke() + } else { + val newItems = + alertItems.mapIndexed { itemIndex, item -> + item.copy(isSelected = itemIndex == index) + } + + alertItems = newItems + } + }, + onItemCheckedChanged = { index -> + val newItems = alertItems.toMutableList() + val oldItem = newItems[index] + newItems[index] = + oldItem.copy(isSelected = !oldItem.isSelected) + + alertItems = newItems.toImmutableList() + } + ) + Spacer(modifier = Modifier.height(10.dp)) + } else { + customContent?.let { content -> + Spacer(modifier = Modifier.height(4.dp)) + content.invoke(this) + Spacer(modifier = Modifier.height(10.dp)) + } + } + } + + if (canScrollForward) { + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + + Row { + Spacer(modifier = Modifier.width(20.dp)) + neutralText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false + } + neutralAction?.invoke() + } + ) { + Text(text = text) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + cancelText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false + } + cancelAction?.invoke() + } + ) { + Text(text = text) + } + } + + Spacer(modifier = Modifier.width(2.dp)) + + confirmText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false + } + confirmAction?.invoke() + } + ) { + Text(text = text) + } + } + + Spacer(modifier = Modifier.width(20.dp)) + } + } + } + } + } + } +} + +@Composable +fun AlertAnimation( + visible: Boolean, + content: @Composable AnimatedVisibilityScope.() -> Unit +) { + AnimatedVisibility( + visible = visible, + enter = fadeIn(animationSpec = tween(400)) + + scaleIn(animationSpec = tween(400)), + exit = fadeOut(animationSpec = tween(150)), + content = content + ) +} + +@Preview +@Composable +fun AlertItemsPreview() { + AppTheme { + AlertItems( + selectionType = ItemsSelectionType.None, + items = ImmutableList(5) { index -> + DialogItem( + title = "Item #${index + 1}", + isSelected = index % 2 == 0 + ) + }, + onItemClick = {} + ) + } +} + +@Composable +private fun AlertItems( + selectionType: ItemsSelectionType, + items: ImmutableList, + onItemClick: ((index: Int) -> Unit)? = null, + onItemCheckedChanged: ((index: Int) -> Unit)? = null +) { + items.forEachIndexed { index, item -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .clickable { + if (selectionType == ItemsSelectionType.Multi) { + onItemCheckedChanged?.invoke(index) + } else { + onItemClick?.invoke(index) + } + }, + verticalAlignment = Alignment.CenterVertically + ) { + // TODO: 29/12/2023, Danil Nikolaev: check onClick & onCheckedChange actions + when (selectionType) { + ItemsSelectionType.Multi -> { + Spacer(modifier = Modifier.width(10.dp)) + Checkbox( + checked = item.isSelected, + onCheckedChange = {} + ) + } + + ItemsSelectionType.Single -> { + Spacer(modifier = Modifier.width(10.dp)) + RadioButton( + selected = item.isSelected, + onClick = {} + ) + } + + ItemsSelectionType.None -> { + Spacer(modifier = Modifier.width(26.dp)) + } + } + Spacer(modifier = Modifier.width(4.dp)) + Text( + modifier = Modifier.weight(1f), + text = item.title, + style = MaterialTheme.typography.bodyLarge + ) + Spacer(modifier = Modifier.width(20.dp)) + } + } +} + +data class DialogItem( + val title: String, + val isSelected: Boolean +) + +sealed interface ItemsSelectionType { + data object Single : ItemsSelectionType + data object Multi : ItemsSelectionType + data object None : ItemsSelectionType +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TabItem.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TabItem.kt new file mode 100644 index 00000000..5d32f63c --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TabItem.kt @@ -0,0 +1,7 @@ +package com.meloda.app.fast.designsystem + +data class TabItem( + val titleResId: Int?, + val unselectedIconResId: Int?, + val selectedIconResId: Int? +) diff --git a/app/src/main/kotlin/com/meloda/fast/ui/widgets/TextFieldErrorText.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TextFieldErrorText.kt similarity index 83% rename from app/src/main/kotlin/com/meloda/fast/ui/widgets/TextFieldErrorText.kt rename to core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TextFieldErrorText.kt index d130759f..212d8a13 100644 --- a/app/src/main/kotlin/com/meloda/fast/ui/widgets/TextFieldErrorText.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TextFieldErrorText.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.ui.widgets +package com.meloda.app.fast.designsystem import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -7,6 +7,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -14,7 +15,7 @@ import androidx.compose.ui.unit.dp @Composable fun TextFieldErrorText( modifier: Modifier = Modifier, - text: String = "Field must not be empty", + text: String = stringResource(id = R.string.error_empty_field), withSpacer: Boolean = true ) { Row { diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/colorschemes/ClassicColorScheme.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/colorschemes/ClassicColorScheme.kt new file mode 100644 index 00000000..0c26289c --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/colorschemes/ClassicColorScheme.kt @@ -0,0 +1,155 @@ +package com.meloda.app.fast.designsystem.colorschemes + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +object ClassicColorScheme { + private val primaryLight = Color(0xFF405F90) + private val onPrimaryLight = Color(0xFFFFFFFF) + private val primaryContainerLight = Color(0xFFD6E3FF) + private val onPrimaryContainerLight = Color(0xFF001B3D) + private val secondaryLight = Color(0xFF555F71) + private val onSecondaryLight = Color(0xFFFFFFFF) + private val secondaryContainerLight = Color(0xFFDAE2F9) + private val onSecondaryContainerLight = Color(0xFF121C2B) + private val tertiaryLight = Color(0xFF6F5575) + private val onTertiaryLight = Color(0xFFFFFFFF) + private val tertiaryContainerLight = Color(0xFFF9D8FD) + private val onTertiaryContainerLight = Color(0xFF28132F) + private val errorLight = Color(0xFFBA1A1A) + private val onErrorLight = Color(0xFFFFFFFF) + private val errorContainerLight = Color(0xFFFFDAD6) + private val onErrorContainerLight = Color(0xFF410002) + private val backgroundLight = Color(0xFFF9F9FF) + private val onBackgroundLight = Color(0xFF191C20) + private val surfaceLight = Color(0xFFF9F9FF) + private val onSurfaceLight = Color(0xFF191C20) + private val surfaceVariantLight = Color(0xFFE0E2EC) + private val onSurfaceVariantLight = Color(0xFF44474E) + private val outlineLight = Color(0xFF74777F) + private val outlineVariantLight = Color(0xFFC4C6CF) + private val scrimLight = Color(0xFF000000) + private val inverseSurfaceLight = Color(0xFF2E3036) + private val inverseOnSurfaceLight = Color(0xFFF0F0F7) + private val inversePrimaryLight = Color(0xFFA9C7FF) + private val surfaceDimLight = Color(0xFFD9D9E0) + private val surfaceBrightLight = Color(0xFFF9F9FF) + private val surfaceContainerLowestLight = Color(0xFFFFFFFF) + private val surfaceContainerLowLight = Color(0xFFF3F3FA) + private val surfaceContainerLight = Color(0xFFEDEDF4) + private val surfaceContainerHighLight = Color(0xFFE7E8EE) + private val surfaceContainerHighestLight = Color(0xFFE2E2E9) + + private val primaryDark = Color(0xFFA9C7FF) + private val onPrimaryDark = Color(0xFF08305F) + private val primaryContainerDark = Color(0xFF274777) + private val onPrimaryContainerDark = Color(0xFFD6E3FF) + private val secondaryDark = Color(0xFFBDC7DC) + private val onSecondaryDark = Color(0xFF283141) + private val secondaryContainerDark = Color(0xFF3E4758) + private val onSecondaryContainerDark = Color(0xFFDAE2F9) + private val tertiaryDark = Color(0xFFDCBCE1) + private val onTertiaryDark = Color(0xFF3F2845) + private val tertiaryContainerDark = Color(0xFF563E5C) + private val onTertiaryContainerDark = Color(0xFFF9D8FD) + private val errorDark = Color(0xFFFFB4AB) + private val onErrorDark = Color(0xFF690005) + private val errorContainerDark = Color(0xFF93000A) + private val onErrorContainerDark = Color(0xFFFFDAD6) + private val backgroundDark = Color(0xFF111318) + private val onBackgroundDark = Color(0xFFE2E2E9) + private val surfaceDark = Color(0xFF111318) + private val onSurfaceDark = Color(0xFFE2E2E9) + private val surfaceVariantDark = Color(0xFF44474E) + private val onSurfaceVariantDark = Color(0xFFC4C6CF) + private val outlineDark = Color(0xFF8E9099) + private val outlineVariantDark = Color(0xFF44474E) + private val scrimDark = Color(0xFF000000) + private val inverseSurfaceDark = Color(0xFFE2E2E9) + private val inverseOnSurfaceDark = Color(0xFF2E3036) + private val inversePrimaryDark = Color(0xFF405F90) + private val surfaceDimDark = Color(0xFF111318) + private val surfaceBrightDark = Color(0xFF37393E) + private val surfaceContainerLowestDark = Color(0xFF0C0E13) + private val surfaceContainerLowDark = Color(0xFF191C20) + private val surfaceContainerDark = Color(0xFF1D2024) + private val surfaceContainerHighDark = Color(0xFF282A2F) + private val surfaceContainerHighestDark = Color(0xFF33353A) + + val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, + ) + + val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, + ) +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/BlurrableTopAppBar.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/BlurrableTopAppBar.kt new file mode 100644 index 00000000..1684249b --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/BlurrableTopAppBar.kt @@ -0,0 +1,81 @@ +package com.meloda.app.fast.designsystem.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.meloda.app.fast.designsystem.LocalTheme +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class +) +@Composable +fun BlurrableTopAppBar( + modifier: Modifier = Modifier, + title: String, + listState: LazyListState?, + hazeState: HazeState = remember { HazeState() } +) { + val currentTheme = LocalTheme.current + + val toolbarColorAlpha by animateFloatAsState( + targetValue = if (listState == null || !listState.canScrollBackward) 1f else 0f, + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + + val toolbarContainerColor by animateColorAsState( + targetValue = + if (currentTheme.usingBlur || listState != null && !listState.canScrollBackward) + MaterialTheme.colorScheme.surface + else + MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + + TopAppBar( + title = { + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = toolbarContainerColor.copy( + alpha = if (currentTheme.usingBlur) toolbarColorAlpha else 1f + ) + ), + modifier = modifier + .then( + if (currentTheme.usingBlur) { + Modifier.hazeChild( + state = hazeState, + style = HazeMaterials.thick() + ) + } else { + Modifier + } + ) + .fillMaxWidth(), + ) +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/FullScreenLoader.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/FullScreenLoader.kt new file mode 100644 index 00000000..9c487b95 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/FullScreenLoader.kt @@ -0,0 +1,21 @@ +package com.meloda.app.fast.designsystem.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun FullScreenLoader(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/NoItemsView.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/NoItemsView.kt new file mode 100644 index 00000000..d60de134 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/NoItemsView.kt @@ -0,0 +1,27 @@ +package com.meloda.app.fast.designsystem.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.meloda.app.fast.designsystem.R + +@Composable +fun NoItemsView( + modifier: Modifier = Modifier, + customText: String? = null +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = customText ?: stringResource(id = R.string.no_items), + style = MaterialTheme.typography.titleLarge + ) + } +} diff --git a/core/designsystem/src/main/res/drawable/baseline_account_circle_24.xml b/core/designsystem/src/main/res/drawable/baseline_account_circle_24.xml new file mode 100644 index 00000000..1e24cf39 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/baseline_account_circle_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/baseline_chat_24.xml b/core/designsystem/src/main/res/drawable/baseline_chat_24.xml new file mode 100644 index 00000000..7f6fda16 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/baseline_chat_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/baseline_people_alt_24.xml b/core/designsystem/src/main/res/drawable/baseline_people_alt_24.xml new file mode 100644 index 00000000..90c82148 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/baseline_people_alt_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_account_circle_cut.xml b/core/designsystem/src/main/res/drawable/ic_account_circle_cut.xml similarity index 100% rename from app/src/main/res/drawable/ic_account_circle_cut.xml rename to core/designsystem/src/main/res/drawable/ic_account_circle_cut.xml diff --git a/app/src/main/res/drawable/ic_arrow_end.xml b/core/designsystem/src/main/res/drawable/ic_arrow_end.xml similarity index 100% rename from app/src/main/res/drawable/ic_arrow_end.xml rename to core/designsystem/src/main/res/drawable/ic_arrow_end.xml diff --git a/app/src/main/res/drawable/ic_attachment_audio.xml b/core/designsystem/src/main/res/drawable/ic_attachment_audio.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_audio.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_audio.xml diff --git a/app/src/main/res/drawable/ic_attachment_call.xml b/core/designsystem/src/main/res/drawable/ic_attachment_call.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_call.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_call.xml diff --git a/app/src/main/res/drawable/ic_attachment_file.xml b/core/designsystem/src/main/res/drawable/ic_attachment_file.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_file.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_file.xml diff --git a/app/src/main/res/drawable/ic_attachment_forwarded_message.xml b/core/designsystem/src/main/res/drawable/ic_attachment_forwarded_message.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_forwarded_message.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_forwarded_message.xml diff --git a/app/src/main/res/drawable/ic_attachment_forwarded_messages.xml b/core/designsystem/src/main/res/drawable/ic_attachment_forwarded_messages.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_forwarded_messages.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_forwarded_messages.xml diff --git a/app/src/main/res/drawable/ic_attachment_gift.xml b/core/designsystem/src/main/res/drawable/ic_attachment_gift.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_gift.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_gift.xml diff --git a/app/src/main/res/drawable/ic_attachment_graffiti.xml b/core/designsystem/src/main/res/drawable/ic_attachment_graffiti.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_graffiti.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_graffiti.xml diff --git a/app/src/main/res/drawable/ic_attachment_group_call.xml b/core/designsystem/src/main/res/drawable/ic_attachment_group_call.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_group_call.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_group_call.xml diff --git a/app/src/main/res/drawable/ic_attachment_link.xml b/core/designsystem/src/main/res/drawable/ic_attachment_link.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_link.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_link.xml diff --git a/app/src/main/res/drawable/ic_attachment_mini_app.xml b/core/designsystem/src/main/res/drawable/ic_attachment_mini_app.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_mini_app.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_mini_app.xml diff --git a/app/src/main/res/drawable/ic_attachment_photo.xml b/core/designsystem/src/main/res/drawable/ic_attachment_photo.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_photo.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_photo.xml diff --git a/app/src/main/res/drawable/ic_attachment_poll.xml b/core/designsystem/src/main/res/drawable/ic_attachment_poll.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_poll.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_poll.xml diff --git a/app/src/main/res/drawable/ic_attachment_sticker.xml b/core/designsystem/src/main/res/drawable/ic_attachment_sticker.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_sticker.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_sticker.xml diff --git a/app/src/main/res/drawable/ic_attachment_story.xml b/core/designsystem/src/main/res/drawable/ic_attachment_story.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_story.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_story.xml diff --git a/app/src/main/res/drawable/ic_attachment_video.xml b/core/designsystem/src/main/res/drawable/ic_attachment_video.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_video.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_video.xml diff --git a/app/src/main/res/drawable/ic_attachment_voice.xml b/core/designsystem/src/main/res/drawable/ic_attachment_voice.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_voice.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_voice.xml diff --git a/app/src/main/res/drawable/ic_attachment_wall.xml b/core/designsystem/src/main/res/drawable/ic_attachment_wall.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_wall.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_wall.xml diff --git a/app/src/main/res/drawable/ic_attachment_wall_reply.xml b/core/designsystem/src/main/res/drawable/ic_attachment_wall_reply.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_wall_reply.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_wall_reply.xml diff --git a/app/src/main/res/drawable/ic_baseline_attach_file_24.xml b/core/designsystem/src/main/res/drawable/ic_baseline_attach_file_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_baseline_attach_file_24.xml rename to core/designsystem/src/main/res/drawable/ic_baseline_attach_file_24.xml diff --git a/app/src/main/res/drawable/ic_baseline_create_24.xml b/core/designsystem/src/main/res/drawable/ic_baseline_create_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_baseline_create_24.xml rename to core/designsystem/src/main/res/drawable/ic_baseline_create_24.xml diff --git a/app/src/main/res/drawable/ic_fast_logo.xml b/core/designsystem/src/main/res/drawable/ic_fast_logo.xml similarity index 100% rename from app/src/main/res/drawable/ic_fast_logo.xml rename to core/designsystem/src/main/res/drawable/ic_fast_logo.xml diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_foreground.xml rename to core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/app/src/main/res/drawable/ic_logo_big.xml b/core/designsystem/src/main/res/drawable/ic_logo_big.xml similarity index 100% rename from app/src/main/res/drawable/ic_logo_big.xml rename to core/designsystem/src/main/res/drawable/ic_logo_big.xml diff --git a/app/src/main/res/drawable/ic_map_marker.xml b/core/designsystem/src/main/res/drawable/ic_map_marker.xml similarity index 100% rename from app/src/main/res/drawable/ic_map_marker.xml rename to core/designsystem/src/main/res/drawable/ic_map_marker.xml diff --git a/app/src/main/res/drawable/ic_outline_emoji_emotions_24.xml b/core/designsystem/src/main/res/drawable/ic_outline_emoji_emotions_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_outline_emoji_emotions_24.xml rename to core/designsystem/src/main/res/drawable/ic_outline_emoji_emotions_24.xml diff --git a/app/src/main/res/drawable/ic_round_add_circle_outline_24.xml b/core/designsystem/src/main/res/drawable/ic_round_add_circle_outline_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_add_circle_outline_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_add_circle_outline_24.xml diff --git a/app/src/main/res/drawable/ic_round_arrow_back_24.xml b/core/designsystem/src/main/res/drawable/ic_round_arrow_back_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_arrow_back_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_arrow_back_24.xml diff --git a/app/src/main/res/drawable/ic_round_bookmark_border_24.xml b/core/designsystem/src/main/res/drawable/ic_round_bookmark_border_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_bookmark_border_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_bookmark_border_24.xml diff --git a/app/src/main/res/drawable/ic_round_close_24.xml b/core/designsystem/src/main/res/drawable/ic_round_close_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_close_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_close_24.xml diff --git a/app/src/main/res/drawable/ic_round_done_24.xml b/core/designsystem/src/main/res/drawable/ic_round_done_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_done_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_done_24.xml diff --git a/app/src/main/res/drawable/ic_round_mic_none_24.xml b/core/designsystem/src/main/res/drawable/ic_round_mic_none_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_mic_none_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_mic_none_24.xml diff --git a/app/src/main/res/drawable/ic_round_person_24.xml b/core/designsystem/src/main/res/drawable/ic_round_person_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_person_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_person_24.xml diff --git a/app/src/main/res/drawable/ic_round_push_pin_24.xml b/core/designsystem/src/main/res/drawable/ic_round_push_pin_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_push_pin_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_push_pin_24.xml diff --git a/core/designsystem/src/main/res/drawable/outline_account_circle_24.xml b/core/designsystem/src/main/res/drawable/outline_account_circle_24.xml new file mode 100644 index 00000000..c85da5ee --- /dev/null +++ b/core/designsystem/src/main/res/drawable/outline_account_circle_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/outline_chat_24.xml b/core/designsystem/src/main/res/drawable/outline_chat_24.xml new file mode 100644 index 00000000..7ce81fa5 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/outline_chat_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/outline_people_alt_24.xml b/core/designsystem/src/main/res/drawable/outline_people_alt_24.xml new file mode 100644 index 00000000..f3e073ee --- /dev/null +++ b/core/designsystem/src/main/res/drawable/outline_people_alt_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/round_more_vert_24.xml b/core/designsystem/src/main/res/drawable/pin_off_outline_24.xml similarity index 52% rename from app/src/main/res/drawable/round_more_vert_24.xml rename to core/designsystem/src/main/res/drawable/pin_off_outline_24.xml index b29b2aef..872faead 100644 --- a/app/src/main/res/drawable/round_more_vert_24.xml +++ b/core/designsystem/src/main/res/drawable/pin_off_outline_24.xml @@ -1,3 +1,4 @@ + + android:pathData="M8,6.2V4H7V2H17V4H16V12L18,14V16H17.8L14,12.2V4H10V8.2L8,6.2M20,20.7L18.7,22L12.8,16.1V22H11.2V16H6V14L8,12V11.3L2,5.3L3.3,4L20,20.7M8.8,14H10.6L9.7,13.1L8.8,14Z" /> diff --git a/app/src/main/res/drawable/ic_trash_can_outline_24.xml b/core/designsystem/src/main/res/drawable/pin_outline_24.xml similarity index 65% rename from app/src/main/res/drawable/ic_trash_can_outline_24.xml rename to core/designsystem/src/main/res/drawable/pin_outline_24.xml index 05862a21..fb9b7e19 100644 --- a/app/src/main/res/drawable/ic_trash_can_outline_24.xml +++ b/core/designsystem/src/main/res/drawable/pin_outline_24.xml @@ -6,5 +6,5 @@ android:viewportHeight="24"> - \ No newline at end of file + android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12M8.8,14L10,12.8V4H14V12.8L15.2,14H8.8Z" /> + diff --git a/core/designsystem/src/main/res/drawable/round_attach_file_24.xml b/core/designsystem/src/main/res/drawable/round_attach_file_24.xml new file mode 100644 index 00000000..9e4d42bc --- /dev/null +++ b/core/designsystem/src/main/res/drawable/round_attach_file_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_cake_24.xml b/core/designsystem/src/main/res/drawable/round_cake_24.xml similarity index 100% rename from app/src/main/res/drawable/round_cake_24.xml rename to core/designsystem/src/main/res/drawable/round_cake_24.xml diff --git a/core/designsystem/src/main/res/drawable/round_delete_outline_24.xml b/core/designsystem/src/main/res/drawable/round_delete_outline_24.xml new file mode 100644 index 00000000..8ab6f1f0 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/round_delete_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_done_all_24.xml b/core/designsystem/src/main/res/drawable/round_done_all_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_done_all_24.xml rename to core/designsystem/src/main/res/drawable/round_done_all_24.xml diff --git a/app/src/main/res/drawable/round_file_download_24.xml b/core/designsystem/src/main/res/drawable/round_file_download_24.xml similarity index 100% rename from app/src/main/res/drawable/round_file_download_24.xml rename to core/designsystem/src/main/res/drawable/round_file_download_24.xml diff --git a/app/src/main/res/drawable/round_install_mobile_24.xml b/core/designsystem/src/main/res/drawable/round_install_mobile_24.xml similarity index 100% rename from app/src/main/res/drawable/round_install_mobile_24.xml rename to core/designsystem/src/main/res/drawable/round_install_mobile_24.xml diff --git a/app/src/main/res/drawable/round_qr_code_24.xml b/core/designsystem/src/main/res/drawable/round_qr_code_24.xml similarity index 100% rename from app/src/main/res/drawable/round_qr_code_24.xml rename to core/designsystem/src/main/res/drawable/round_qr_code_24.xml diff --git a/app/src/main/res/drawable/round_restart_alt_24.xml b/core/designsystem/src/main/res/drawable/round_restart_alt_24.xml similarity index 100% rename from app/src/main/res/drawable/round_restart_alt_24.xml rename to core/designsystem/src/main/res/drawable/round_restart_alt_24.xml diff --git a/core/designsystem/src/main/res/drawable/round_send_24.xml b/core/designsystem/src/main/res/drawable/round_send_24.xml new file mode 100644 index 00000000..94dcd656 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/round_send_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/round_sms_24.xml b/core/designsystem/src/main/res/drawable/round_sms_24.xml similarity index 100% rename from app/src/main/res/drawable/round_sms_24.xml rename to core/designsystem/src/main/res/drawable/round_sms_24.xml diff --git a/app/src/main/res/drawable/round_visibility_24.xml b/core/designsystem/src/main/res/drawable/round_visibility_24.xml similarity index 100% rename from app/src/main/res/drawable/round_visibility_24.xml rename to core/designsystem/src/main/res/drawable/round_visibility_24.xml diff --git a/app/src/main/res/drawable/round_visibility_off_24.xml b/core/designsystem/src/main/res/drawable/round_visibility_off_24.xml similarity index 100% rename from app/src/main/res/drawable/round_visibility_off_24.xml rename to core/designsystem/src/main/res/drawable/round_visibility_off_24.xml diff --git a/app/src/main/res/drawable/round_vpn_key_24.xml b/core/designsystem/src/main/res/drawable/round_vpn_key_24.xml similarity index 100% rename from app/src/main/res/drawable/round_vpn_key_24.xml rename to core/designsystem/src/main/res/drawable/round_vpn_key_24.xml diff --git a/app/src/main/res/drawable/test_captcha.webp b/core/designsystem/src/main/res/drawable/test_captcha.webp similarity index 100% rename from app/src/main/res/drawable/test_captcha.webp rename to core/designsystem/src/main/res/drawable/test_captcha.webp diff --git a/app/src/main/res/font/google_sans_bold.ttf b/core/designsystem/src/main/res/font/google_sans_bold.ttf similarity index 100% rename from app/src/main/res/font/google_sans_bold.ttf rename to core/designsystem/src/main/res/font/google_sans_bold.ttf diff --git a/app/src/main/res/font/google_sans_bold_italic.ttf b/core/designsystem/src/main/res/font/google_sans_bold_italic.ttf similarity index 100% rename from app/src/main/res/font/google_sans_bold_italic.ttf rename to core/designsystem/src/main/res/font/google_sans_bold_italic.ttf diff --git a/app/src/main/res/font/google_sans_italic.ttf b/core/designsystem/src/main/res/font/google_sans_italic.ttf similarity index 100% rename from app/src/main/res/font/google_sans_italic.ttf rename to core/designsystem/src/main/res/font/google_sans_italic.ttf diff --git a/app/src/main/res/font/google_sans_medium.ttf b/core/designsystem/src/main/res/font/google_sans_medium.ttf similarity index 100% rename from app/src/main/res/font/google_sans_medium.ttf rename to core/designsystem/src/main/res/font/google_sans_medium.ttf diff --git a/app/src/main/res/font/google_sans_medium_italic.ttf b/core/designsystem/src/main/res/font/google_sans_medium_italic.ttf similarity index 100% rename from app/src/main/res/font/google_sans_medium_italic.ttf rename to core/designsystem/src/main/res/font/google_sans_medium_italic.ttf diff --git a/app/src/main/res/font/google_sans_regular.ttf b/core/designsystem/src/main/res/font/google_sans_regular.ttf similarity index 100% rename from app/src/main/res/font/google_sans_regular.ttf rename to core/designsystem/src/main/res/font/google_sans_regular.ttf diff --git a/app/src/main/res/font/roboto_black.ttf b/core/designsystem/src/main/res/font/roboto_black.ttf similarity index 100% rename from app/src/main/res/font/roboto_black.ttf rename to core/designsystem/src/main/res/font/roboto_black.ttf diff --git a/app/src/main/res/font/roboto_black_italic.ttf b/core/designsystem/src/main/res/font/roboto_black_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_black_italic.ttf rename to core/designsystem/src/main/res/font/roboto_black_italic.ttf diff --git a/app/src/main/res/font/roboto_bold.ttf b/core/designsystem/src/main/res/font/roboto_bold.ttf similarity index 100% rename from app/src/main/res/font/roboto_bold.ttf rename to core/designsystem/src/main/res/font/roboto_bold.ttf diff --git a/app/src/main/res/font/roboto_bold_italic.ttf b/core/designsystem/src/main/res/font/roboto_bold_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_bold_italic.ttf rename to core/designsystem/src/main/res/font/roboto_bold_italic.ttf diff --git a/app/src/main/res/font/roboto_italic.ttf b/core/designsystem/src/main/res/font/roboto_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_italic.ttf rename to core/designsystem/src/main/res/font/roboto_italic.ttf diff --git a/app/src/main/res/font/roboto_light.ttf b/core/designsystem/src/main/res/font/roboto_light.ttf similarity index 100% rename from app/src/main/res/font/roboto_light.ttf rename to core/designsystem/src/main/res/font/roboto_light.ttf diff --git a/app/src/main/res/font/roboto_light_italic.ttf b/core/designsystem/src/main/res/font/roboto_light_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_light_italic.ttf rename to core/designsystem/src/main/res/font/roboto_light_italic.ttf diff --git a/app/src/main/res/font/roboto_medium.ttf b/core/designsystem/src/main/res/font/roboto_medium.ttf similarity index 100% rename from app/src/main/res/font/roboto_medium.ttf rename to core/designsystem/src/main/res/font/roboto_medium.ttf diff --git a/app/src/main/res/font/roboto_medium_italic.ttf b/core/designsystem/src/main/res/font/roboto_medium_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_medium_italic.ttf rename to core/designsystem/src/main/res/font/roboto_medium_italic.ttf diff --git a/app/src/main/res/font/roboto_regular.ttf b/core/designsystem/src/main/res/font/roboto_regular.ttf similarity index 100% rename from app/src/main/res/font/roboto_regular.ttf rename to core/designsystem/src/main/res/font/roboto_regular.ttf diff --git a/app/src/main/res/font/roboto_thin.ttf b/core/designsystem/src/main/res/font/roboto_thin.ttf similarity index 100% rename from app/src/main/res/font/roboto_thin.ttf rename to core/designsystem/src/main/res/font/roboto_thin.ttf diff --git a/app/src/main/res/font/roboto_thin_italic.ttf b/core/designsystem/src/main/res/font/roboto_thin_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_thin_italic.ttf rename to core/designsystem/src/main/res/font/roboto_thin_italic.ttf diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/core/designsystem/src/main/res/values-ru/strings.xml b/core/designsystem/src/main/res/values-ru/strings.xml new file mode 100644 index 00000000..02888443 --- /dev/null +++ b/core/designsystem/src/main/res/values-ru/strings.xml @@ -0,0 +1,90 @@ + + + Вложение + Вложения + Настройки + Произошла ошибка + Ошибка: %s + Ошибка + Пароль + Код капчи + Введите код с картинки + Логин + Поле не должно быть пустым + Код + Введите код из смс + Вы + Геолокация + Точка + Сообщение + Сообщения + Нет сообщений + Сообщение самоуничтожилось + Обновить + Вчера + Сегодня + Г + М + Н + Д + Сейчас + Начните печатать здесь… + Введите логин + Введите пароль + Введите код + Нужна валидация + Произошла неизвестная ошибка + Ошибка авторизации + Токен недействителен + %s создал(-а) «%s» + %s переименовал(-а) чат в «%s» + %s обновил(-а) фото беседы + %s удалил(-а) фото беседы + %s вышел(-ла) из чата + %s исключил %s + %s вернулся(-лась) в беседу + %s пригласил(-а) %s + %s присоединился(-лась) в беседу через ссылку + %s присоединился(-лась) к звонку + %s присоединился(-лась) к звонку через ссылку + %s закрепил(-а) сообщение + %s открепил(-а) сообщение + %s сделал(-а) скриншот беседы + %s изменил(-а) тему беседы + Фотография + %d фотографии + %d фотографий + %d фотографий + Видео + Аудиозапись + Файл + Голосовое сообщение + Ссылка + Мини-приложение + Стикер + Подарок + Запись на стене + Граффити + Опрос + Комментарий + Звонок + Текущий звонок + Событие + Куратор + История + Виджет + Место + Запись сообщества + Запись пользователя + Запись на стене + Выйти + Подтверждение + Динамические цвета + Цвета для приложения будут извлечены из ваших обоев на главном экране + Язык приложения + Текущий: %s + Системный + Применить + Открыть системный пикер языка + Язык приложения + diff --git a/app/src/main/res/values/arrays.xml b/core/designsystem/src/main/res/values-uk/strings.xml similarity index 60% rename from app/src/main/res/values/arrays.xml rename to core/designsystem/src/main/res/values-uk/strings.xml index 0d2c4cc4..3ea04e70 100644 --- a/app/src/main/res/values/arrays.xml +++ b/core/designsystem/src/main/res/values-uk/strings.xml @@ -1,4 +1,2 @@ - - - \ No newline at end of file + diff --git a/app/src/main/res/values/ic_launcher_background.xml b/core/designsystem/src/main/res/values/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/values/ic_launcher_background.xml rename to core/designsystem/src/main/res/values/ic_launcher_background.xml diff --git a/app/src/main/res/values/plurals.xml b/core/designsystem/src/main/res/values/plurals.xml similarity index 100% rename from app/src/main/res/values/plurals.xml rename to core/designsystem/src/main/res/values/plurals.xml diff --git a/app/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml similarity index 75% rename from app/src/main/res/values/strings.xml rename to core/designsystem/src/main/res/values/strings.xml index 34a0556d..6209e291 100644 --- a/app/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -1,15 +1,10 @@ Fast + Fast Messenger + Attachment Attachments - Friends - Chats - Profile - Favorites - Settings - Static Settings - Error occurred Error: %s @@ -17,18 +12,17 @@ Password - Log in - Captcha code Input code from picture Login + Field must not be empty + Code Input code from sms - You Geolocation @@ -42,15 +36,6 @@ Messages self-destructed - Yesterday - - Today - - Y - M - W - D - Now Start typing here… Input login Input password @@ -99,7 +84,7 @@ %d files - Voice message + Voice message Link Mini App Sticker @@ -115,6 +100,18 @@ Story Widget Place + Artist + Playlist + Podcast + + Uploading file + Uploading photo + Uploading video + Typing + Recording + + %s are typing + %s is typing %d bytes @@ -140,6 +137,7 @@ Delete for all Mark as spam + Mark as read Delete Delete @@ -219,4 +217,43 @@ Value + Dark theme + Enabled + Follow system + Follow battery saver + Disabled + Current value: %s + Unknown + AMOLED dark theme + Use AMOLED theme with a pure black background when dark theme is enabled + + Dynamic colors + The colors for the app will be extracted from your home screen wallpaper + + Application Language + Current: %s + + System + English + Russian + Ukrainian + + System + English + Русский + Українська + + Apply + Open system language picker + Application language + Refresh + Members: %d + Loading… + Conversations + Friends + Profile + All + Online + No items + diff --git a/core/designsystem/src/main/res/values/themes.xml b/core/designsystem/src/main/res/values/themes.xml new file mode 100644 index 00000000..46d12ce8 --- /dev/null +++ b/core/designsystem/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +