diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..522972957d3b4abbccb5df2212a90f764102cf69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,87 @@ +# Created by .ignore support plugin (hsz.mobi) +### Android template +.idea/codeStyles/Project.xml +.idea/* + +# Gradle files +.gradle/ +build/ +lint/ +.DS_Store + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +#Bindings NDF +app/ndf/ndf.json +app/ndf/prod.json + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +*.jks +*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md +fastlane/.env + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +/google-services-prod.json +/google-services-release.json +/messenger.keystore +/*.lock +app/src/main/res/raw/ud_contact_test.bin + +app/src/main/res/raw/ud_elixxir_io.crt + +fastlane/Fastfile diff --git a/README.md b/README.md index bf209af4663a0ce4cc2548a5095c9e195fa7d29b..fae8da97f1635fba806ff7ea061c3eda2896196f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # xxmessenger (Android) -***Current Version:*** 2.04/530 (MainNet)<br> +***Current Version:*** 2.1/550 (MainNet)<br> ***Device Orientation:*** Portrait<br> ***API Target:*** Android 26+ (Oreo) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b6f92f280b8f4ae10f0b1051bcf3e747277069d..7a86b5520446a710eac2e2a3c17ccf887832843e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,8 +35,8 @@ android { defaultConfig { applicationId = "io.xxlabs.messenger" - versionCode = 530 - versionName = "2.04" + versionCode = 550 + versionName = "2.1" minSdk = 26 targetSdk = 31 testInstrumentationRunner = "io.xxlabs.messenger.CustomTestRunner" @@ -237,9 +237,14 @@ dependencies { implementation("androidx.security:security-crypto:1.1.0-alpha01") // Room - implementation("androidx.room:room-runtime:2.4.1") + val roomVersion = "2.4.1" + implementation("androidx.room:room-runtime:$roomVersion") implementation("androidx.legacy:legacy-support-v4:1.0.0") - kapt("androidx.room:room-compiler:2.4.1") + implementation("androidx.room:room-ktx:$roomVersion") + kapt("androidx.room:room-compiler:$roomVersion") + implementation("androidx.room:room-runtime:$roomVersion") + kapt("androidx.room:room-compiler:$roomVersion") + implementation("androidx.room:room-ktx:$roomVersion") // Image Handling implementation("com.github.CanHub:Android-Image-Cropper:3.2.1") @@ -253,6 +258,7 @@ dependencies { implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") //Time Source implementation("com.lyft.kronos:kronos-android:0.0.1-alpha10") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8f7fac9274f38065625f59bcc4ffd34ac15eceeb..ca5377551f8808b0ae26bbdda73f9b228957e69d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -83,7 +83,7 @@ <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> - <activity android:name=".backup.auth.DropboxAuthActivity" /> + <activity android:name=".backup.cloud.dropbox.DropboxAuthActivity" /> <meta-data android:name="QUERY_LOG" diff --git a/app/src/main/java/io/xxlabs/messenger/application/AppDatabase.kt b/app/src/main/java/io/xxlabs/messenger/application/AppDatabase.kt index 116bad9f815b89b0f71c273907fcc6a019a1d84a..6346925b9a149dfc0d5f6315b0299a16324beed2 100644 --- a/app/src/main/java/io/xxlabs/messenger/application/AppDatabase.kt +++ b/app/src/main/java/io/xxlabs/messenger/application/AppDatabase.kt @@ -20,9 +20,15 @@ import net.sqlcipher.database.SupportFactory @TypeConverters(DateConverter::class, DateTimeConverter::class, SentStatusConverter::class) @Database( - entities = [ContactData::class, PrivateMessageData::class, GroupMember::class, - GroupData::class, GroupMessageData::class], - version = 1, + entities = [ + ContactData::class, + PrivateMessageData::class, + GroupMember::class, + GroupData::class, + GroupMessageData::class, + RequestData::class + ], + version = 2, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { @@ -31,6 +37,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun groupsDao(): GroupsDao abstract fun groupMembersDao(): GroupMembersDao abstract fun groupMessagesDao(): GroupMessagesDao + abstract fun requestsDao(): RequestsDao companion object { @Volatile diff --git a/app/src/main/java/io/xxlabs/messenger/backup/BackupModule.kt b/app/src/main/java/io/xxlabs/messenger/backup/BackupModule.kt index 0f8036f50c83220ae8501579230a075f26b70df9..0426014d7136c4c5f54b698edafa43fa94527352 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/BackupModule.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/BackupModule.kt @@ -4,29 +4,39 @@ import dagger.Binds import dagger.Module import io.xxlabs.messenger.backup.bindings.BindingsBackupMediator import io.xxlabs.messenger.backup.bindings.BackupService -import io.xxlabs.messenger.backup.data.BackupDataSource -import io.xxlabs.messenger.backup.data.BackupRepository -import io.xxlabs.messenger.backup.data.RestoreRepository -import io.xxlabs.messenger.backup.model.BackupOption -import io.xxlabs.messenger.backup.model.RestoreOption +import io.xxlabs.messenger.backup.data.backup.* +import io.xxlabs.messenger.backup.data.restore.RestoreManager +import io.xxlabs.messenger.backup.data.restore.RestoreMediator +import io.xxlabs.messenger.repository.PreferencesRepository import javax.inject.Singleton @Module interface BackupModule { + @Singleton + @Binds + fun backupService( + service: BindingsBackupMediator + ): BackupService @Singleton @Binds - fun backupDataSource( - backupDataSource: BackupRepository - ): BackupDataSource<BackupOption> + fun backupManager( + backupManager: BackupMediator + ): BackupManager @Binds - fun restoreDataSource( - restoreDataSource: RestoreRepository - ): BackupDataSource<RestoreOption> + fun restoreManager( + restoreManager: RestoreMediator + ): RestoreManager + @Singleton @Binds - fun backupService( - service: BindingsBackupMediator - ): BackupService + fun backupTaskPubisher( + backupTaskEventManager: BackupTaskEventManager + ): BackupTaskPublisher + + @Binds + fun backupPreferencesRepository( + preferencesRepository: PreferencesRepository + ): BackupPreferencesRepository } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BackupService.kt b/app/src/main/java/io/xxlabs/messenger/backup/bindings/BackupService.kt index c6393ffbc1673f4bcd0dd7aab5229b50c1344324..0e5d7fee7940a22c6bfd4bdc6ff004cca8fc1669 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BackupService.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/bindings/BackupService.kt @@ -1,6 +1,6 @@ package io.xxlabs.messenger.backup.bindings -import io.xxlabs.messenger.backup.data.RestoreLogger +import io.xxlabs.messenger.backup.data.restore.RestoreLogger import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase interface BackupService { diff --git a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsBackupHandler.kt b/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsBackupHandler.kt index 09f57bd4f539c055f7f88b0ff2a63c66ea1ccb5a..f90781508d545200c8b28f2829026a5e1799bd4a 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsBackupHandler.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsBackupHandler.kt @@ -4,7 +4,7 @@ import bindings.Backup import bindings.Bindings import bindings.Client import bindings.UpdateBackupFunc -import io.xxlabs.messenger.backup.data.ExtrasJson +import io.xxlabs.messenger.backup.data.restore.ExtrasJson import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBindings import io.xxlabs.messenger.repository.PreferencesRepository diff --git a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsBackupMediator.kt b/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsBackupMediator.kt index 34b8af4ec818dadc386d27f726bbb911a3982b56..09b18cd5489e593e69ebdc41b172e4b3dffc5798 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsBackupMediator.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsBackupMediator.kt @@ -1,6 +1,6 @@ package io.xxlabs.messenger.backup.bindings -import io.xxlabs.messenger.backup.data.RestoreLogger +import io.xxlabs.messenger.backup.data.restore.RestoreLogger import io.xxlabs.messenger.bindings.listeners.MessageReceivedListener import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase import io.xxlabs.messenger.repository.DaoRepository diff --git a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsRestoreHandler.kt b/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsRestoreHandler.kt index c78a25459ca79f6dc8f22081da01f057c098001b..45a20d821dafc7a8c8015bccdd4b4b7feacdb05d 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsRestoreHandler.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/bindings/BindingsRestoreHandler.kt @@ -1,8 +1,8 @@ package io.xxlabs.messenger.backup.bindings import bindings.* -import io.xxlabs.messenger.backup.data.BackupReport -import io.xxlabs.messenger.backup.data.RestoreLogger +import io.xxlabs.messenger.backup.data.restore.BackupReport +import io.xxlabs.messenger.backup.data.restore.RestoreLogger import io.xxlabs.messenger.bindings.listeners.MessageReceivedListener import io.xxlabs.messenger.bindings.wrapper.bindings.BindingsWrapperBindings import io.xxlabs.messenger.bindings.wrapper.client.ClientWrapperBindings diff --git a/app/src/main/java/io/xxlabs/messenger/backup/cloud/AuthHandler.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/AuthHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..c5f2334f14e247c0b5538633ec64a73a06903920 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/AuthHandler.kt @@ -0,0 +1,12 @@ +package io.xxlabs.messenger.backup.cloud + +import android.content.Intent + +/** + * Handles authentication with a [BackupLocation] that requires sign-in. + */ +interface AuthHandler { + val signInIntent: Intent + fun handleSignInResult(data: Intent?) + fun signOut() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/cloud/AuthResultCallback.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/AuthResultCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..0074d38d06d7a06686ef3422a21d67865bd79b8f --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/AuthResultCallback.kt @@ -0,0 +1,9 @@ +package io.xxlabs.messenger.backup.cloud + +/** + * Exposes the result of an [AuthHandler] authentication attempt. + */ +interface AuthResultCallback { + fun onFailure(errorMsg: String) + fun onSuccess() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/auth/CloudAuthentication.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/CloudAuthentication.kt similarity index 94% rename from app/src/main/java/io/xxlabs/messenger/backup/auth/CloudAuthentication.kt rename to app/src/main/java/io/xxlabs/messenger/backup/cloud/CloudAuthentication.kt index 3ebeed64fb4bb36051a1b2178680b9f35c08f2ce..43f6da2a6c98c408a5f52f17e19dfad2143478b4 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/auth/CloudAuthentication.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/CloudAuthentication.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.auth +package io.xxlabs.messenger.backup.cloud import android.content.Context import android.content.Intent @@ -8,7 +8,6 @@ import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContract import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import io.xxlabs.messenger.backup.model.AuthHandler /** * Encapsulates authentication-related ActivityResultLaunchers diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/CloudStorage.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/CloudStorage.kt similarity index 79% rename from app/src/main/java/io/xxlabs/messenger/backup/data/CloudStorage.kt rename to app/src/main/java/io/xxlabs/messenger/backup/cloud/CloudStorage.kt index 3f3dab1a2f69a0c8e3d5da35aeae19f3fe92b907..0deb331d78154b132b41d31076895912b4ce2374 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/CloudStorage.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/CloudStorage.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.data +package io.xxlabs.messenger.backup.cloud import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -6,10 +6,15 @@ import io.xxlabs.messenger.backup.bindings.AccountArchive import io.xxlabs.messenger.backup.bindings.BackupService import io.xxlabs.messenger.backup.bindings.RestoreParams import io.xxlabs.messenger.backup.bindings.RestoreTaskCallback +import io.xxlabs.messenger.backup.data.backup.BackupOption +import io.xxlabs.messenger.backup.data.restore.RestoreEnvironment +import io.xxlabs.messenger.backup.data.restore.RestoreLog +import io.xxlabs.messenger.backup.data.restore.RestoreOption import io.xxlabs.messenger.backup.model.* -import io.xxlabs.messenger.bindings.wrapper.bindings.bindingsErrorMessage import kotlinx.coroutines.* -import java.lang.IllegalArgumentException +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import java.lang.Exception const val BACKUP_DIRECTORY_NAME = "backup" @@ -28,11 +33,17 @@ abstract class CloudStorage( ) override val lastBackup: LiveData<BackupSnapshot?> by ::_lastBackup - protected val _lastBackup = MutableLiveData<BackupSnapshot?>() + private val _lastBackup = MutableLiveData<BackupSnapshot?>() + + override val lastBackupFlow: StateFlow<BackupSnapshot?> by ::_lastBackupFlow + private val _lastBackupFlow = MutableStateFlow<BackupSnapshot?>(null) override val progress: LiveData<BackupProgress?> by ::_progress protected val _progress = MutableLiveData<BackupProgress?>(null) + override val progressFlow: StateFlow<BackupProgress?> by ::_progressFlow + private val _progressFlow = MutableStateFlow<BackupProgress?>(null) + protected val authResultCallback: AuthResultCallback = AuthResultCallbackDelegate(::onAuthResultFailure, ::authResultSuccess) protected var _authResultCallback: AuthResultCallback? = null @@ -48,7 +59,7 @@ abstract class CloudStorage( _progress.postValue(ProgressData( contactsRestored, total, - error?.run { java.lang.Exception(this) }, + error?.run { Exception(this) }, false, { }, )) @@ -93,19 +104,30 @@ abstract class CloudStorage( ) } + protected fun updateLastBackup(snapshot: BackupSnapshot?) { + _lastBackup.postValue(snapshot) + scope.launch { + _lastBackupFlow.emit(snapshot) + } + } + protected fun updateProgress( progress: Long = 0L, total: Long = 100L, error: Throwable? = null, indeterminate: Boolean = false ) { - _progress.postValue(ProgressData( + val progressData = ProgressData( progress, total, error, indeterminate, {} - )) + ) + _progress.postValue(progressData) + scope.launch { + _progressFlow.emit(progressData) + } } protected data class BackupLocationData( diff --git a/app/src/main/java/io/xxlabs/messenger/backup/auth/GoogleAuthHandler.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/drive/GoogleAuthHandler.kt similarity index 93% rename from app/src/main/java/io/xxlabs/messenger/backup/auth/GoogleAuthHandler.kt rename to app/src/main/java/io/xxlabs/messenger/backup/cloud/drive/GoogleAuthHandler.kt index 30401ef12647fdb13bb465cc80f66f886e46f6cf..17d9a987abd647ffe92c7b80c98869e0c81ebf06 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/auth/GoogleAuthHandler.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/drive/GoogleAuthHandler.kt @@ -1,12 +1,12 @@ -package io.xxlabs.messenger.backup.auth +package io.xxlabs.messenger.backup.cloud.drive import android.content.Intent import com.google.android.gms.auth.api.signin.* import com.google.android.gms.common.api.ApiException import com.google.android.gms.common.api.Scope import com.google.api.services.drive.DriveScopes -import io.xxlabs.messenger.backup.model.AuthHandler -import io.xxlabs.messenger.backup.model.AuthResultCallback +import io.xxlabs.messenger.backup.cloud.AuthHandler +import io.xxlabs.messenger.backup.cloud.AuthResultCallback import io.xxlabs.messenger.support.appContext import timber.log.Timber diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/GoogleDrive.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/drive/GoogleDrive.kt similarity index 95% rename from app/src/main/java/io/xxlabs/messenger/backup/data/GoogleDrive.kt rename to app/src/main/java/io/xxlabs/messenger/backup/cloud/drive/GoogleDrive.kt index d66cfa7b38f341e798047339a5c0def57b3b2891..469dd9c0c7c3bb13632a0d0a204f3aa726f1f48a 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/GoogleDrive.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/drive/GoogleDrive.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.data +package io.xxlabs.messenger.backup.cloud.drive import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.api.client.extensions.android.http.AndroidHttp @@ -9,8 +9,11 @@ import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.google.api.services.drive.model.FileList import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.auth.GoogleAuthHandler import io.xxlabs.messenger.backup.bindings.* +import io.xxlabs.messenger.backup.cloud.BACKUP_DIRECTORY_NAME +import io.xxlabs.messenger.backup.cloud.CloudStorage +import io.xxlabs.messenger.backup.data.backup.BackupPreferencesRepository +import io.xxlabs.messenger.backup.data.restore.RestoreEnvironment import io.xxlabs.messenger.backup.model.* import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.support.appContext @@ -37,7 +40,7 @@ private const val TYPE_BINARY_CONTENT = "application/octet-stream" */ class GoogleDrive private constructor( private val backupService: BackupService, - private val preferences: PreferencesRepository + private val preferences: BackupPreferencesRepository ) : CloudStorage(backupService) { private val authHandler: GoogleAuthHandler by lazy { GoogleAuthHandler(authResultCallback) } @@ -193,7 +196,7 @@ class GoogleDrive private constructor( private fun DriveFile.onSuccessfulUpload() { driveBackupData = DriveBackupData.fromDriveFile(this@onSuccessfulUpload) - _lastBackup.postValue(driveBackupData) + updateLastBackup(driveBackupData) Timber.d("Backup successful: $id") updateProgress(100) } @@ -222,7 +225,7 @@ class GoogleDrive private constructor( accessDriveAppData(Drive::queryBackupFile)?.apply { if (isNotEmpty()) { driveBackupData = DriveBackupData.fromDriveFile(first()) - _lastBackup.postValue(driveBackupData) + updateLastBackup(driveBackupData) } withContext(Dispatchers.Main) { _authResultCallback?.onSuccess() @@ -237,7 +240,7 @@ class GoogleDrive private constructor( @Volatile private var instance: GoogleDrive? = null - fun getInstance(backupService: BackupService, preferences: PreferencesRepository): GoogleDrive = + fun getInstance(backupService: BackupService, preferences: BackupPreferencesRepository): GoogleDrive = instance ?: GoogleDrive(backupService, preferences).also { instance = it } } } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/Dropbox.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/Dropbox.kt similarity index 93% rename from app/src/main/java/io/xxlabs/messenger/backup/data/Dropbox.kt rename to app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/Dropbox.kt index 90c15497681c95abcf7f7fa839820b86fe48d0a3..9e4027a5d1ef76370c14331f4f6dd79631e0d743 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/Dropbox.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/Dropbox.kt @@ -1,7 +1,5 @@ -package io.xxlabs.messenger.backup.data +package io.xxlabs.messenger.backup.cloud.dropbox -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import com.dropbox.core.DbxRequestConfig import com.dropbox.core.android.Auth import com.dropbox.core.oauth.DbxCredential @@ -12,12 +10,14 @@ import com.dropbox.core.v2.files.ListFolderErrorException import com.dropbox.core.v2.files.WriteMode import io.xxlabs.messenger.BuildConfig import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.auth.DropboxAuthHandler import io.xxlabs.messenger.backup.bindings.AccountArchive import io.xxlabs.messenger.backup.bindings.BACKUP_FILE_NAME import io.xxlabs.messenger.backup.bindings.BackupService +import io.xxlabs.messenger.backup.cloud.BACKUP_DIRECTORY_NAME +import io.xxlabs.messenger.backup.cloud.CloudStorage +import io.xxlabs.messenger.backup.data.backup.BackupPreferencesRepository +import io.xxlabs.messenger.backup.data.restore.RestoreEnvironment import io.xxlabs.messenger.backup.model.* -import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.support.appContext import kotlinx.coroutines.* import timber.log.Timber @@ -29,7 +29,7 @@ import java.io.File */ class Dropbox private constructor( private val backupService: BackupService, - private val preferences: PreferencesRepository + private val preferences: BackupPreferencesRepository ) : CloudStorage(backupService) { private var credential: DbxCredential? = null @@ -96,7 +96,7 @@ class Dropbox private constructor( dbxInstance?.run { try { dbxBackupData = getBackup() - _lastBackup.postValue(dbxBackupData) + updateLastBackup(dbxBackupData) } catch (e: Exception) { if (e is ListFolderErrorException) createBackupFolder() else clearCredentialCache() @@ -154,7 +154,7 @@ class Dropbox private constructor( upload(file).apply { Timber.d("Uploaded $name. Id: $id. Size: $size.") dbxBackupData = DropboxBackupData.from(this) - _lastBackup.postValue(dbxBackupData) + updateLastBackup(dbxBackupData) updateProgress(100) } } @@ -186,7 +186,7 @@ class Dropbox private constructor( private var instance: Dropbox? = null const val CLIENT_IDENTIFIER = "xxMessengerAndroid/${BuildConfig.VERSION_CODE}" - fun getInstance(backupService: BackupService, preferences: PreferencesRepository): Dropbox = + fun getInstance(backupService: BackupService, preferences: BackupPreferencesRepository): Dropbox = instance ?: Dropbox(backupService, preferences).also { instance = it } } } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/auth/DropboxAuthActivity.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/DropboxAuthActivity.kt similarity index 93% rename from app/src/main/java/io/xxlabs/messenger/backup/auth/DropboxAuthActivity.kt rename to app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/DropboxAuthActivity.kt index 00a25d560bf1220a7e6451132b392bddee4b8850..f22fb8a1303ae54a1d394ee623ad39c510fa4d3b 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/auth/DropboxAuthActivity.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/DropboxAuthActivity.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.auth +package io.xxlabs.messenger.backup.cloud.dropbox import android.app.Activity import android.content.Intent @@ -6,7 +6,7 @@ import android.os.Bundle import com.dropbox.core.DbxRequestConfig import com.dropbox.core.android.Auth import io.xxlabs.messenger.BuildConfig -import io.xxlabs.messenger.backup.data.Dropbox.Companion.CLIENT_IDENTIFIER +import io.xxlabs.messenger.backup.cloud.dropbox.Dropbox.Companion.CLIENT_IDENTIFIER /** * Dropbox Auth activity wrapper diff --git a/app/src/main/java/io/xxlabs/messenger/backup/auth/DropboxAuthHandler.kt b/app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/DropboxAuthHandler.kt similarity index 68% rename from app/src/main/java/io/xxlabs/messenger/backup/auth/DropboxAuthHandler.kt rename to app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/DropboxAuthHandler.kt index bb40519c5fae1a774e0274d50652ae81a9ddbb53..a851f479fcb26a838db6f39cdf1359466643ae8c 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/auth/DropboxAuthHandler.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/cloud/dropbox/DropboxAuthHandler.kt @@ -1,10 +1,10 @@ -package io.xxlabs.messenger.backup.auth +package io.xxlabs.messenger.backup.cloud.dropbox import android.content.Intent -import io.xxlabs.messenger.backup.auth.DropboxAuthActivity.Companion.EXTRA_DBX_CREDENTIAL -import io.xxlabs.messenger.backup.auth.DropboxAuthActivity.Companion.START_OAUTH_INTENT -import io.xxlabs.messenger.backup.model.AuthHandler -import io.xxlabs.messenger.backup.model.AuthResultCallback +import io.xxlabs.messenger.backup.cloud.dropbox.DropboxAuthActivity.Companion.EXTRA_DBX_CREDENTIAL +import io.xxlabs.messenger.backup.cloud.dropbox.DropboxAuthActivity.Companion.START_OAUTH_INTENT +import io.xxlabs.messenger.backup.cloud.AuthHandler +import io.xxlabs.messenger.backup.cloud.AuthResultCallback import io.xxlabs.messenger.support.appContext import timber.log.Timber diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/AccountBackupDataSource.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/AccountBackupDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..b45eac48bb7da321258216d33ae6931b167bbd8e --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/AccountBackupDataSource.kt @@ -0,0 +1,9 @@ +package io.xxlabs.messenger.backup.data + +import io.xxlabs.messenger.backup.model.AccountBackup + +interface AccountBackupDataSource { + val locations: List<AccountBackup> + fun getBackupFrom(source: BackupSource): AccountBackup + fun getSourceFor(backup: AccountBackup): BackupSource? +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupDataSource.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/BackupDataSource.kt deleted file mode 100644 index 9eaaedf1e37a66aeb9c2d4e4c8a181f2a2793652..0000000000000000000000000000000000000000 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupDataSource.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.xxlabs.messenger.backup.data - -import androidx.lifecycle.LiveData -import io.xxlabs.messenger.backup.model.BackupLocation -import io.xxlabs.messenger.backup.model.AccountBackup -import io.xxlabs.messenger.backup.model.BackupSettings - -interface BackupDataSource<T : AccountBackup> : BackupTaskPublisher { - val locations: List<BackupLocation> - val settings: LiveData<BackupSettings> - - fun getActiveOption(): T? - fun setLocation(location: BackupLocation): T - suspend fun enableBackup(backup: T, password: String) - fun disableBackup(backup: AccountBackup) - fun getBackupDetails(location: BackupLocation): T - fun setNetwork(network: BackupSettings.Network) - fun setFrequency(frequency: BackupSettings.Frequency) -} diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupLocationRepository.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/BackupLocationRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..a4c4340f0bb4198725af93dbf38ec0dfc4960805 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/BackupLocationRepository.kt @@ -0,0 +1,34 @@ +package io.xxlabs.messenger.backup.data + +import io.xxlabs.messenger.backup.bindings.BackupService +import io.xxlabs.messenger.backup.cloud.drive.GoogleDrive +import io.xxlabs.messenger.backup.cloud.dropbox.Dropbox +import io.xxlabs.messenger.backup.data.backup.BackupPreferencesRepository +import io.xxlabs.messenger.backup.model.AccountBackup + +abstract class BackupLocationRepository( + preferences: BackupPreferencesRepository, + backupService: BackupService, +) : AccountBackupDataSource { + + protected val googleDrive = GoogleDrive.getInstance(backupService, preferences) + protected val dropbox = Dropbox.getInstance(backupService, preferences) + + override val locations: List<AccountBackup> = listOf( + googleDrive, + dropbox + ) + + override fun getBackupFrom(source: BackupSource): AccountBackup = + when (source) { + BackupSource.DRIVE -> googleDrive + BackupSource.DROPBOX -> dropbox + } + + override fun getSourceFor(backup: AccountBackup): BackupSource? = + when (backup) { + is GoogleDrive -> BackupSource.DRIVE + is Dropbox -> BackupSource.DROPBOX + else -> null + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupRepository.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/BackupRepository.kt deleted file mode 100644 index 625be4f73722037784fe8bd951715b9f40a76259..0000000000000000000000000000000000000000 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupRepository.kt +++ /dev/null @@ -1,233 +0,0 @@ -package io.xxlabs.messenger.backup.data - -import android.content.Context -import android.net.ConnectivityManager -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.bindings.BackupScheduler -import io.xxlabs.messenger.backup.bindings.BackupService -import io.xxlabs.messenger.backup.model.* -import io.xxlabs.messenger.repository.PreferencesRepository -import io.xxlabs.messenger.support.appContext -import java.security.InvalidParameterException -import javax.inject.Inject - -/** - * Mediates between, local backup [BackupService], backup-related [preferences] and the - * [AccountBackup] saved to a remote [CloudStorage]. - */ -abstract class AccountBackupRepository<T : AccountBackup>( - private val preferences: PreferencesRepository, - private val backupService: BackupService, - private val backupTaskEventManager: BackupTaskEventManager = BackupTaskEventManager() -) : BackupDataSource<T>, - BackupService by backupService, - BackupTaskPublisher by backupTaskEventManager { - - private val settingsHandler: BackupPreferences by lazy { - PreferencesDelegate(preferences, backupService, this) - } - override val settings: LiveData<BackupSettings> by settingsHandler::settings - - protected val googleDrive = GoogleDrive.getInstance(backupService, preferences) - protected val dropbox = Dropbox.getInstance(backupService, preferences) - - override val locations: List<BackupLocation> = listOf( - googleDrive.location, - dropbox.location - ) - - var currentSelection: T? = null - private set - - private val backupScheduler: BackupScheduler by lazy { - BackupScheduler(preferences, this) - } - - private val PreferencesRepository.backupOption: BackupOption? - get() = when (backupLocation) { - googleDrive.location.name -> googleDrive - dropbox.location.name -> dropbox - else -> null - } - - init { - backupTaskEventManager.subscribe(backupScheduler) - backupService.setListener(backupTaskEventManager) - } - - fun getActiveBackupOption() = preferences.backupOption - - protected fun saveLocation(service: T): T { - preferences.backupLocation = service.location.name - return service - } - - override suspend fun enableBackup(backup: T, password: String) { - backupService.initializeBackup(password) - saveLocation(backup) - settingsHandler.setEnabled(true, backup) - } - - override fun disableBackup(backup: AccountBackup) { - backupService.stopBackup() - settingsHandler.setEnabled(false, backup) - } - - override fun setNetwork(network: BackupSettings.Network) { - settingsHandler.setNetwork(network) - } - - override fun setFrequency(frequency: BackupSettings.Frequency) { - settingsHandler.setFrequency(frequency) - } -} - -class BackupRepository @Inject constructor( - private val preferences: PreferencesRepository, - private val backupService: BackupService, -) : AccountBackupRepository<BackupOption>(preferences, backupService) { - - override fun getActiveOption(): BackupOption? = getActiveBackupOption() - - override fun setLocation(location: BackupLocation): BackupOption { - return when (location.name) { - appContext().getString(R.string.backup_service_google_drive) -> saveLocation(googleDrive) - appContext().getString(R.string.backup_service_dropbox) -> saveLocation(dropbox) - else -> throw InvalidParameterException("No service found for selected location.") - } - } - - override fun getBackupDetails(location: BackupLocation): BackupOption { - return when (location.name) { - appContext().getString(R.string.backup_service_google_drive) -> googleDrive - appContext().getString(R.string.backup_service_dropbox) -> dropbox - else -> throw InvalidParameterException("No service found for selected location.") - } - } -} - -class RestoreRepository @Inject constructor( - preferences: PreferencesRepository, - backupService: BackupService -) : AccountBackupRepository<RestoreOption>(preferences, backupService) { - - override fun getActiveOption(): RestoreOption? = null - - override fun setLocation(location: BackupLocation): RestoreOption { - return when (location.name) { - appContext().getString(R.string.backup_service_google_drive) -> googleDrive - appContext().getString(R.string.backup_service_dropbox) -> dropbox - else -> throw InvalidParameterException("No service found for selected location.") - } - } - - override fun getBackupDetails(location: BackupLocation): RestoreOption { - return when (location.name) { - appContext().getString(R.string.backup_service_google_drive) -> googleDrive - appContext().getString(R.string.backup_service_dropbox) -> dropbox - else -> throw InvalidParameterException("No service found for selected location.") - } - } -} - -interface BackupPreferences { - val settings: LiveData<BackupSettings> - fun setEnabled(enabled: Boolean, backup: AccountBackup) - fun setNetwork(network: BackupSettings.Network) - fun setFrequency(frequency: BackupSettings.Frequency) -} - -/** - * Encapsulates backup-related preferences - */ -private class PreferencesDelegate( - private val preferences: PreferencesRepository, - private val backupService: BackupService, - private val backupDataSource: AccountBackupRepository<*>, -) : BackupPreferences { - private val _settings = MutableLiveData(backupSettings) - override val settings: LiveData<BackupSettings> by ::_settings - - private val network: ConnectivityManager by lazy { - appContext().getSystemService(Context.CONNECTIVITY_SERVICE) - as ConnectivityManager - } - - private val backupSettings: BackupSettings - get() { - return object : BackupSettings { - override val frequency: BackupSettings.Frequency - get() = if (preferences.autoBackup) BackupSettings.Frequency.AUTOMATIC - else BackupSettings.Frequency.MANUAL - override val network: BackupSettings.Network - get() = if (preferences.wiFiOnlyBackup) BackupSettings.Network.WIFI_ONLY - else BackupSettings.Network.ANY - } - } - - private fun tryBackup() { - if (preferences.autoBackup && networkPreferencesMet()) { - backupDataSource.getActiveBackupOption()?.backupNow() - } - } - - override fun setEnabled(enabled: Boolean, backup: AccountBackup) { - preferences.isBackupEnabled = enabled - when (backup) { - is GoogleDrive -> googleDriveEnabled(enabled) - is Dropbox -> dropboxEnabled(enabled) - } - reflectChanges() - if (enabled) tryBackup() - } - - private fun googleDriveEnabled(enabled: Boolean) { - preferences.isGoogleDriveEnabled = enabled - if (enabled) preferences.isDropboxEnabled = false - } - - private fun dropboxEnabled(enabled: Boolean) { - preferences.isDropboxEnabled = enabled - if (enabled) preferences.isGoogleDriveEnabled = false - } - - override fun setNetwork(network: BackupSettings.Network) { - preferences.wiFiOnlyBackup = when (network) { - BackupSettings.Network.WIFI_ONLY -> true - else -> false - } - tryBackup() - reflectChanges() - } - - override fun setFrequency(frequency: BackupSettings.Frequency) { - preferences.autoBackup = when (frequency) { - BackupSettings.Frequency.AUTOMATIC -> true - else -> false - } - tryBackup() - reflectChanges() - } - - private fun reflectChanges() { - _settings.postValue(backupSettings) - } - - private fun networkPreferencesMet(): Boolean { - return when (preferences.wiFiOnlyBackup) { - true -> network.isWiFi() - false -> network.isOnline() - } - } - - private fun ConnectivityManager.isOnline(): Boolean { - return activeNetworkInfo?.isConnected == true - } - - private fun ConnectivityManager.isWiFi(): Boolean { - return isOnline() - && getNetworkInfo(activeNetwork)?.type == ConnectivityManager.TYPE_WIFI - } -} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupSource.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/BackupSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..4f08a132fedd1497c507dd49f3ccb66dc8e47563 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/BackupSource.kt @@ -0,0 +1,5 @@ +package io.xxlabs.messenger.backup.data + +import java.io.Serializable + +enum class BackupSource : Serializable { DRIVE, DROPBOX } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/DummyAccountBackup.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/DummyAccountBackup.kt deleted file mode 100644 index a48a4c3edae16740b10235f3e74677fae08a2d85..0000000000000000000000000000000000000000 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/DummyAccountBackup.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.xxlabs.messenger.backup.data - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import io.xxlabs.messenger.backup.model.* -import io.xxlabs.messenger.backup.model.BackupSettings.* - -fun generateBackupOption(location: BackupLocation): BackupOption = - DummyBackupOption(DummyAccountBackup(location)) - -fun generateDummyRestoreOption(location: BackupLocation): RestoreOption = - DummyRestoreOption(DummyAccountBackup(location)) - -data class DummyAccountBackup( - override val location: BackupLocation -) : AccountBackup { - override val lastBackup: LiveData<BackupSnapshot?> = - MutableLiveData<BackupSnapshot>(DummySnapshot()) - override val progress: LiveData<BackupProgress?> = MutableLiveData(DummyProgress()) - override fun isEnabled() = true -} - -data class DummySnapshot( - override val date: Long = System.currentTimeMillis() - 50_000, - override val sizeBytes: Long = 300_000L -): BackupSnapshot - -data class DummyBackupOption( - private val accountBackup: AccountBackup -): BackupOption, AccountBackup by accountBackup { - override fun backupNow() {} -} - -data class DummyRestoreOption( - private val accountBackup: AccountBackup -): RestoreOption, AccountBackup by accountBackup { - override val restoreLog: RestoreLog = RestoreLogger() - override suspend fun restore(environment: RestoreEnvironment) {} - override fun cancelRestore() { } -} - -data class DummyProgress ( - override val bytesTransferred: Long = 100L, - override val bytesTotal: Long = 300L, - override val error: Throwable? = null, - override val indeterminate: Boolean = true, -): BackupProgress { - override fun cancel() {} -} - -data class DummySettings( - override val frequency: Frequency = Frequency.MANUAL, - override val network: Network = Network.ANY, -): BackupSettings - diff --git a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BackupScheduler.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/AccountBackupScheduler.kt similarity index 58% rename from app/src/main/java/io/xxlabs/messenger/backup/bindings/BackupScheduler.kt rename to app/src/main/java/io/xxlabs/messenger/backup/data/backup/AccountBackupScheduler.kt index a1423891bd1bb92e08d27be52945dcc83c5cf0a3..6018a125f212d95ee2d8a91cfe70e65cd176fda5 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/bindings/BackupScheduler.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/AccountBackupScheduler.kt @@ -1,21 +1,12 @@ -package io.xxlabs.messenger.backup.bindings +package io.xxlabs.messenger.backup.data.backup import android.content.Context import android.net.ConnectivityManager -import io.xxlabs.messenger.backup.data.AccountBackupRepository -import io.xxlabs.messenger.backup.data.BackupRepository -import io.xxlabs.messenger.backup.data.BackupTaskListener -import io.xxlabs.messenger.backup.model.BackupOption -import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.support.appContext -/** - * Automatically uploads backups to the active [BackupOption] - * if the network & frequency preferences are met. - */ -class BackupScheduler( - private val preferences: PreferencesRepository, - private val backupRepository: AccountBackupRepository<*> +class AccountBackupScheduler( + private val preferences: BackupPreferencesRepository, + private val backupManager: BackupManager ) : BackupTaskListener { private val network: ConnectivityManager by lazy { @@ -25,7 +16,7 @@ class BackupScheduler( override fun onComplete() { if (preferences.autoBackup && networkPreferencesMet()) { - backupRepository.getActiveBackupOption()?.backupNow() + backupManager.getActiveBackupOption()?.backupNow() } } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupManager.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3f14443b82c08f4c96c42e07cbdee90ceb76a26 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupManager.kt @@ -0,0 +1,15 @@ +package io.xxlabs.messenger.backup.data.backup + +import io.xxlabs.messenger.backup.data.AccountBackupDataSource +import io.xxlabs.messenger.backup.model.AccountBackup +import kotlinx.coroutines.flow.Flow + +interface BackupManager : AccountBackupDataSource, BackupTaskPublisher { + val settings: Flow<BackupSettings> + suspend fun enableBackup(backup: AccountBackup, password: String) + fun getActiveBackupOption(): BackupOption? + fun disableBackup(backup: AccountBackup) + fun setNetwork(network: BackupSettings.Network) + fun setFrequency(frequency: BackupSettings.Frequency) + fun backupNow(backup: AccountBackup) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupMediator.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupMediator.kt new file mode 100644 index 0000000000000000000000000000000000000000..6812febe5f7a7e7cd7fe17a937c04af4b22def46 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupMediator.kt @@ -0,0 +1,69 @@ +package io.xxlabs.messenger.backup.data.backup + +import io.xxlabs.messenger.backup.bindings.BackupService +import io.xxlabs.messenger.backup.data.BackupLocationRepository +import io.xxlabs.messenger.backup.model.AccountBackup +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class BackupMediator @Inject constructor( + private val preferences: BackupPreferencesRepository, + private val backupService: BackupService, + private val backupTaskPublisher: BackupTaskPublisher +) : BackupLocationRepository(preferences, backupService), + BackupManager, + BackupService by backupService, + BackupTaskPublisher by backupTaskPublisher +{ + private val settingsHandler: BackupPreferencesDelegate by lazy { + BackupPreferencesDelegate(preferences, this) + } + + private val BackupPreferencesRepository.backupOption: BackupOption? + get() = when (backupLocation) { + googleDrive.location.name -> googleDrive + dropbox.location.name -> dropbox + else -> null + } + + private val backupScheduler: AccountBackupScheduler by lazy { + AccountBackupScheduler(preferences, this) + } + + override val settings: Flow<BackupSettings> = settingsHandler.settingsFlow + + init { + backupTaskPublisher.subscribe(backupScheduler) + backupService.setListener(backupTaskPublisher) + } + + override fun getActiveBackupOption() = preferences.backupOption + + override suspend fun enableBackup(backup: AccountBackup, password: String) { + backupService.initializeBackup(password) + saveLocation(backup) + settingsHandler.setEnabled(true, backup) + } + + private fun saveLocation(backup: AccountBackup) { + preferences.backupLocation = backup.location.name + } + + override fun disableBackup(backup: AccountBackup) { + backupService.stopBackup() + settingsHandler.setEnabled(false, backup) + } + + override fun setNetwork(network: BackupSettings.Network) { + settingsHandler.setNetwork(network) + } + + override fun setFrequency(frequency: BackupSettings.Frequency) { + settingsHandler.setFrequency(frequency) + } + + override fun backupNow(backup: AccountBackup) { + saveLocation(backup) + (backup as? BackupOption)?.backupNow() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupOption.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupOption.kt new file mode 100644 index 0000000000000000000000000000000000000000..02372356d2f016606236e32efeda546e7fbdb1f8 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupOption.kt @@ -0,0 +1,11 @@ +package io.xxlabs.messenger.backup.data.backup + +import io.xxlabs.messenger.backup.model.AccountBackup + +/** + * Saves an account to an [AccountBackup]. + */ +interface BackupOption : AccountBackup { + fun isEnabled(): Boolean + fun backupNow() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferences.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferences.kt new file mode 100644 index 0000000000000000000000000000000000000000..84c36fc5ddc42b88097ab72db6441f2517dc4c47 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferences.kt @@ -0,0 +1,13 @@ +package io.xxlabs.messenger.backup.data.backup + +import androidx.lifecycle.LiveData +import io.xxlabs.messenger.backup.model.AccountBackup +import kotlinx.coroutines.flow.StateFlow + +interface BackupPreferences { + val settings: LiveData<BackupSettings> + val settingsFlow: StateFlow<BackupSettings> + fun setEnabled(enabled: Boolean, backup: AccountBackup) + fun setNetwork(network: BackupSettings.Network) + fun setFrequency(frequency: BackupSettings.Frequency) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferencesDelegate.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferencesDelegate.kt new file mode 100644 index 0000000000000000000000000000000000000000..740ca090c7fd7972bc7d4dfea206b644800bcfea --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferencesDelegate.kt @@ -0,0 +1,115 @@ +package io.xxlabs.messenger.backup.data.backup + +import android.content.Context +import android.net.ConnectivityManager +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.xxlabs.messenger.backup.cloud.drive.GoogleDrive +import io.xxlabs.messenger.backup.cloud.dropbox.Dropbox +import io.xxlabs.messenger.backup.model.AccountBackup +import io.xxlabs.messenger.support.appContext +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class BackupPreferencesDelegate( + private val preferences: BackupPreferencesRepository, + private val backupManager: BackupManager, +) : BackupPreferences { + + private val scope = CoroutineScope( + CoroutineName("BackupPreferencesDelegate") + + Job() + + Dispatchers.Default + ) + + private val network: ConnectivityManager by lazy { + appContext().getSystemService(Context.CONNECTIVITY_SERVICE) + as ConnectivityManager + } + + private val backupSettings: BackupSettings + get() { + return object : BackupSettings { + override val frequency: BackupSettings.Frequency + get() = if (preferences.autoBackup) BackupSettings.Frequency.AUTOMATIC + else BackupSettings.Frequency.MANUAL + override val network: BackupSettings.Network + get() = if (preferences.wiFiOnlyBackup) BackupSettings.Network.WIFI_ONLY + else BackupSettings.Network.ANY + } + } + + override val settings: LiveData<BackupSettings> by ::_settings + private val _settings = MutableLiveData(backupSettings) + + override val settingsFlow: StateFlow<BackupSettings> by ::_settingsFlow + private val _settingsFlow = MutableStateFlow(backupSettings) + + private fun tryBackup() { + if (preferences.autoBackup && networkPreferencesMet()) { + backupManager.getActiveBackupOption()?.backupNow() + } + } + + override fun setEnabled(enabled: Boolean, backup: AccountBackup) { + preferences.isBackupEnabled = enabled + when (backup) { + is GoogleDrive -> googleDriveEnabled(enabled) + is Dropbox -> dropboxEnabled(enabled) + } + reflectChanges() + if (enabled) tryBackup() + } + + private fun googleDriveEnabled(enabled: Boolean) { + preferences.isGoogleDriveEnabled = enabled + if (enabled) preferences.isDropboxEnabled = false + } + + private fun dropboxEnabled(enabled: Boolean) { + preferences.isDropboxEnabled = enabled + if (enabled) preferences.isGoogleDriveEnabled = false + } + + override fun setNetwork(network: BackupSettings.Network) { + preferences.wiFiOnlyBackup = when (network) { + BackupSettings.Network.WIFI_ONLY -> true + else -> false + } + tryBackup() + reflectChanges() + } + + override fun setFrequency(frequency: BackupSettings.Frequency) { + preferences.autoBackup = when (frequency) { + BackupSettings.Frequency.AUTOMATIC -> true + else -> false + } + tryBackup() + reflectChanges() + } + + private fun reflectChanges() { + _settings.postValue(backupSettings) + scope.launch { + _settingsFlow.emit(backupSettings) + } + } + + private fun networkPreferencesMet(): Boolean { + return when (preferences.wiFiOnlyBackup) { + true -> network.isWiFi() + false -> network.isOnline() + } + } + + private fun ConnectivityManager.isOnline(): Boolean { + return activeNetworkInfo?.isConnected == true + } + + private fun ConnectivityManager.isWiFi(): Boolean { + return isOnline() + && getNetworkInfo(activeNetwork)?.type == ConnectivityManager.TYPE_WIFI + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferencesRepository.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferencesRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3cad9acc81cf785030c0258b30925274e30a557 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupPreferencesRepository.kt @@ -0,0 +1,13 @@ +package io.xxlabs.messenger.backup.data.backup + +interface BackupPreferencesRepository { + var isBackupEnabled: Boolean + var isGoogleDriveEnabled: Boolean + var isDropboxEnabled: Boolean + var backupPassword: String? + var autoBackup: Boolean + var wiFiOnlyBackup: Boolean + var backupLocation: String? + var dbxCredential: String? + var isUserProfileBackedUp: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupSettings.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupSettings.kt new file mode 100644 index 0000000000000000000000000000000000000000..a74e5cb36e7d84a73c3cfe1702dcc493e25fb9a0 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupSettings.kt @@ -0,0 +1,27 @@ +package io.xxlabs.messenger.backup.data.backup + +import io.xxlabs.messenger.support.extensions.capitalizeWords + +/** + * Preferences that determine if and when the backup runs automatically. + */ +interface BackupSettings { + val frequency: Frequency + val network: Network + + enum class Frequency { + AUTOMATIC, MANUAL; + + override fun toString(): String { + return super.toString().capitalizeWords() + } + } + enum class Network { + WIFI_ONLY { + override fun toString() = "Wi-Fi Only" + }, + ANY { + override fun toString() = "Wi-Fi or Cellular" + }; + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupTaskPublisher.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupTaskPublisher.kt similarity index 81% rename from app/src/main/java/io/xxlabs/messenger/backup/data/BackupTaskPublisher.kt rename to app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupTaskPublisher.kt index 35e426e59ad1b950938cf951bd05b54ef20305c9..0be972fa7cf74c32ae5293a2fc82cf800dc1137f 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupTaskPublisher.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/backup/BackupTaskPublisher.kt @@ -1,10 +1,11 @@ -package io.xxlabs.messenger.backup.data +package io.xxlabs.messenger.backup.data.backup import io.xxlabs.messenger.backup.bindings.AccountArchive import io.xxlabs.messenger.backup.bindings.BackupTaskCallback import timber.log.Timber +import javax.inject.Inject -class BackupTaskEventManager : BackupTaskCallback, BackupTaskPublisher { +class BackupTaskEventManager @Inject constructor() : BackupTaskPublisher { private val listeners = mutableListOf<BackupTaskListener>() override fun onComplete(backupData: AccountArchive) { @@ -28,7 +29,7 @@ class BackupTaskEventManager : BackupTaskCallback, BackupTaskPublisher { } } -interface BackupTaskPublisher { +interface BackupTaskPublisher : BackupTaskCallback { fun subscribe(listener: BackupTaskListener) fun unsubscribe(listener: BackupTaskListener) } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupReport.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/BackupReport.kt similarity index 98% rename from app/src/main/java/io/xxlabs/messenger/backup/data/BackupReport.kt rename to app/src/main/java/io/xxlabs/messenger/backup/data/restore/BackupReport.kt index dfeef53832b341f6ee07e5ccf2a326cc641c87c9..7219f042b3592feb1419e04312432ad3f2bb0d46 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/BackupReport.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/BackupReport.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.data +package io.xxlabs.messenger.backup.data.restore import com.google.gson.Gson import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreEnvironment.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreEnvironment.kt new file mode 100644 index 0000000000000000000000000000000000000000..be128ea425f2830e16f40c8715e3c954ff8b5bbf --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreEnvironment.kt @@ -0,0 +1,11 @@ +package io.xxlabs.messenger.backup.data.restore + +/** + * Necessary data to restore an account to a new session on a device. + */ +data class RestoreEnvironment( + val ndf: String, + val appDirectory: String, + val sessionPassword: ByteArray, + val backupPassword: ByteArray, +) \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/RestoreLogger.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreLogger.kt similarity index 91% rename from app/src/main/java/io/xxlabs/messenger/backup/data/RestoreLogger.kt rename to app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreLogger.kt index 79678386d1f0f6bec8f2cce4dc63b2b6ff90877c..5b57c9c57e011add5f450a40d0f8d37ba9d5b518 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/data/RestoreLogger.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreLogger.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.data +package io.xxlabs.messenger.backup.data.restore import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreManager.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..7e09c1a1618410921e580168ba664af79fc8422f --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreManager.kt @@ -0,0 +1,10 @@ +package io.xxlabs.messenger.backup.data.restore + +import io.xxlabs.messenger.backup.data.AccountBackupDataSource +import io.xxlabs.messenger.backup.model.AccountBackup + +interface RestoreManager : AccountBackupDataSource { + fun getRestoreLog(backup: AccountBackup): RestoreLog? + suspend fun restore(backup: AccountBackup, environment: RestoreEnvironment) + fun cancelRestore(backup: AccountBackup) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreMediator.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreMediator.kt new file mode 100644 index 0000000000000000000000000000000000000000..c6e1b68dd5337c3b78020feb2ecb5c2e1d92ba61 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreMediator.kt @@ -0,0 +1,25 @@ +package io.xxlabs.messenger.backup.data.restore + +import io.xxlabs.messenger.backup.bindings.BackupService +import io.xxlabs.messenger.backup.data.BackupLocationRepository +import io.xxlabs.messenger.backup.data.backup.BackupPreferencesRepository +import io.xxlabs.messenger.backup.model.AccountBackup +import javax.inject.Inject + +class RestoreMediator @Inject constructor( + preferences: BackupPreferencesRepository, + backupService: BackupService +) : BackupLocationRepository(preferences, backupService), + RestoreManager { + + override fun getRestoreLog(backup: AccountBackup): RestoreLog? = + (backup as? RestoreOption)?.restoreLog + + override suspend fun restore(backup: AccountBackup, environment: RestoreEnvironment) { + (backup as? RestoreOption)?.restore(environment) + } + + override fun cancelRestore(backup: AccountBackup) { + (backup as? RestoreOption)?.cancelRestore() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreOption.kt b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreOption.kt new file mode 100644 index 0000000000000000000000000000000000000000..0154523b64a4eb6bb51bf85d91e1a2241c777411 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/backup/data/restore/RestoreOption.kt @@ -0,0 +1,12 @@ +package io.xxlabs.messenger.backup.data.restore + +import io.xxlabs.messenger.backup.model.AccountBackup + +/** + * Restores an account from an [AccountBackup]. + */ +interface RestoreOption : AccountBackup { + val restoreLog: RestoreLog + suspend fun restore(environment: RestoreEnvironment) + fun cancelRestore() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/model/AccountBackup.kt b/app/src/main/java/io/xxlabs/messenger/backup/model/AccountBackup.kt index 067b7d6d0c7b454ebb0ec2acddd4b0921430785a..a43a4c777e62511f4c76eee83e2fd08fc194b4a8 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/model/AccountBackup.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/model/AccountBackup.kt @@ -1,75 +1,28 @@ package io.xxlabs.messenger.backup.model -import android.content.Intent import androidx.lifecycle.LiveData -import io.xxlabs.messenger.backup.data.RestoreLog -import io.xxlabs.messenger.support.extensions.capitalizeWords +import io.xxlabs.messenger.backup.cloud.AuthHandler +import io.xxlabs.messenger.backup.cloud.AuthResultCallback +import kotlinx.coroutines.flow.StateFlow import java.io.Serializable /** * An account backup or restore option. */ -interface AccountBackup : Serializable { +interface AccountBackup { val location: BackupLocation + @Deprecated("Use lastBackupFlow") val lastBackup: LiveData<BackupSnapshot?> + val lastBackupFlow: StateFlow<BackupSnapshot?> + @Deprecated("Use progressFlow") val progress: LiveData<BackupProgress?> - fun isEnabled(): Boolean -} - -/** - * Restores an account from an [AccountBackup]. - */ -interface RestoreOption : AccountBackup { - val restoreLog: RestoreLog - suspend fun restore(environment: RestoreEnvironment) - fun cancelRestore() -} - -/** - * Necessary data to restore an account to a new session on a device. - */ -data class RestoreEnvironment( - val ndf: String, - val appDirectory: String, - val sessionPassword: ByteArray, - val backupPassword: ByteArray, -) - -/** - * Saves an account to an [AccountBackup]. - */ -interface BackupOption : AccountBackup { - fun backupNow() -} - -/** - * Preferences that determine if and when the backup runs automatically. - */ -interface BackupSettings { - val frequency: Frequency - val network: Network - - enum class Frequency { - AUTOMATIC, MANUAL; - - override fun toString(): String { - return super.toString().capitalizeWords() - } - } - enum class Network { - WIFI_ONLY { - override fun toString() = "Wi-Fi Only" - }, - ANY { - override fun toString() = "Wi-Fi or Cellular" - }; - } + val progressFlow: StateFlow<BackupProgress?> } /** * Describes the storage location or provider for an [AccountBackup]. */ -interface BackupLocation : Serializable { +interface BackupLocation { val icon: Int val name: String fun isEnabled(): Boolean @@ -79,27 +32,10 @@ interface BackupLocation : Serializable { fun signOut() } -/** - * Handles authentication with a [BackupLocation] that requires sign-in. - */ -interface AuthHandler { - val signInIntent: Intent - fun handleSignInResult(data: Intent?) - fun signOut() -} - -/** - * Exposes the result of an [AuthHandler] authentication attempt. - */ -interface AuthResultCallback { - fun onFailure(errorMsg: String) - fun onSuccess() -} - /** * Metadata about a backup in a [BackupLocation]. */ -interface BackupSnapshot : Serializable { +interface BackupSnapshot { val date: Long val sizeBytes: Long } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailFragment.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailFragment.kt similarity index 87% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailFragment.kt rename to app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailFragment.kt index b255c3f797210239d979a863f4d5337b3a72cab9..688885afa4719a32af2655ae2ec886803a0d05f7 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailFragment.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import android.os.Bundle import androidx.fragment.app.Fragment @@ -8,6 +8,10 @@ import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.viewModels import io.xxlabs.messenger.R +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialogUI +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialog +import io.xxlabs.messenger.ui.dialog.radiobutton.RadioButtonDialog +import io.xxlabs.messenger.ui.dialog.radiobutton.RadioButtonDialogUI import io.xxlabs.messenger.databinding.FragmentBackupDetailBinding import io.xxlabs.messenger.di.utils.Injectable import io.xxlabs.messenger.support.extensions.toast @@ -25,7 +29,7 @@ class BackupDetailFragment : Fragment(), Injectable { private val backupViewModel: BackupDetailViewModel by viewModels { BackupDetailViewModel.provideFactory( viewModelFactory, - BackupDetailFragmentArgs.fromBundle(requireArguments()).backupOption + BackupDetailFragmentArgs.fromBundle(requireArguments()).source ) } @@ -101,8 +105,8 @@ class BackupDetailFragment : Fragment(), Injectable { .show(childFragmentManager, null) } - private fun showSetPasswordDialog(dialogUI: EditTextTwoButtonDialogUI) { - EditTextTwoButtonInfoDialog.newInstance(dialogUI) + private fun showSetPasswordDialog(dialogUI: TextInputDialogUI) { + TextInputDialog.newInstance(dialogUI) .show(childFragmentManager, null) ui.onPasswordPromptHandled() } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailUI.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailUI.kt similarity index 71% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailUI.kt rename to app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailUI.kt index f6a1db0ea28623365e5b534dc0e8d78d849ec9ac..36e8cfdc7da37b05b693012fa8e97ab852de3b40 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailUI.kt @@ -1,21 +1,24 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import android.text.Spanned import androidx.lifecycle.LiveData -import io.xxlabs.messenger.backup.model.BackupOption -import io.xxlabs.messenger.backup.model.BackupSettings +import io.xxlabs.messenger.backup.data.backup.BackupSettings +import io.xxlabs.messenger.backup.model.AccountBackup +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialogUI +import io.xxlabs.messenger.ui.dialog.radiobutton.RadioButtonDialogUI interface BackupPasswordUI { val isBackupReady: LiveData<Boolean> val isEnabled: LiveData<Boolean> - val showSetPasswordPrompt: LiveData<EditTextTwoButtonDialogUI?> + val showSetPasswordPrompt: LiveData<TextInputDialogUI?> fun onEnableToggled(enabled: Boolean) fun onPasswordPromptHandled() + fun backupNow() } interface BackupDetailUI : BackupPasswordUI { val settings: LiveData<BackupSettings> - val backup: BackupOption + val backup: AccountBackup val description: Spanned // for clickable info button val backupDisclaimer: String val backupFrequencyLabel: String diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailViewModel.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailViewModel.kt similarity index 79% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailViewModel.kt rename to app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailViewModel.kt index ad5daee0bf094d80817e4309e47ef5f6056c39cf..db71456b91a1c501ae9c14b36876665824b3070e 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupDetailViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupDetailViewModel.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import android.text.SpannableString import android.text.Spanned @@ -7,18 +7,24 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.data.BackupDataSource -import io.xxlabs.messenger.backup.model.BackupOption -import io.xxlabs.messenger.backup.model.BackupSettings -import io.xxlabs.messenger.backup.model.BackupSettings.* +import io.xxlabs.messenger.backup.data.BackupSource +import io.xxlabs.messenger.backup.data.backup.BackupManager +import io.xxlabs.messenger.backup.data.backup.BackupOption +import io.xxlabs.messenger.backup.data.backup.BackupSettings +import io.xxlabs.messenger.backup.data.backup.BackupSettings.* +import io.xxlabs.messenger.backup.model.AccountBackup +import io.xxlabs.messenger.ui.dialog.radiobutton.RadioButtonDialogOption +import io.xxlabs.messenger.ui.dialog.radiobutton.RadioButtonDialogUI import io.xxlabs.messenger.support.appContext class BackupDetailViewModel @AssistedInject constructor( - dataSource: BackupDataSource<BackupOption>, - @Assisted override val backup: BackupOption -) : BackupViewModel(dataSource), BackupDetailController { + backupManager: BackupManager, + @Assisted private val source: BackupSource +) : BackupViewModel(backupManager), BackupDetailController { - override val settings: LiveData<BackupSettings> by dataSource::settings + override val backup: AccountBackup get() = backupManager.getBackupFrom(source) + + override val settings: LiveData<BackupSettings> = backupManager.settings.asLiveData() override val description: Spanned = getSpannedDescription() override val backupInProgress: LiveData<Boolean> = Transformations.map(backup.progress) { @@ -105,8 +111,8 @@ class BackupDetailViewModel @AssistedInject constructor( ) { onNetworkSelected(Network.ANY) } } - override fun getBackupOption(): BackupOption { - return backup + override fun getBackupOption(): BackupOption? { + return backup as? BackupOption } private fun getSpannedDescription(): Spanned { @@ -138,20 +144,20 @@ class BackupDetailViewModel @AssistedInject constructor( } private fun onFrequencySelected(frequency: Frequency) { - dataSource.setFrequency(frequency) + backupManager.setFrequency(frequency) } private fun onNetworkSelected(network: Network) { - dataSource.setNetwork(network) + backupManager.setNetwork(network) } companion object { fun provideFactory( assistedFactory: BackupDetailViewModelFactory, - backup: BackupOption + source: BackupSource ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { - return assistedFactory.create(backup) as T + return assistedFactory.create(source) as T } } } @@ -159,5 +165,5 @@ class BackupDetailViewModel @AssistedInject constructor( @AssistedFactory interface BackupDetailViewModelFactory { - fun create(backup: BackupOption): BackupDetailViewModel + fun create(source: BackupSource): BackupDetailViewModel } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupPassword.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupPassword.kt similarity index 93% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupPassword.kt rename to app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupPassword.kt index 983c93dee8fdef2696ddb04a32fa1ee33d770bae..b510f32c2df1e409b2442856c30b19f0000dbccb 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupPassword.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupPassword.kt @@ -1,7 +1,6 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import io.xxlabs.messenger.R -import io.xxlabs.messenger.support.appContext @JvmInline diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsFragment.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsFragment.kt similarity index 88% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsFragment.kt rename to app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsFragment.kt index 0a5e7a668d2a95bf0e367bada4a99604191e0a97..3756aadefe1699b9afd1932e90268839c837f90c 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsFragment.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import android.os.Bundle import androidx.fragment.app.Fragment @@ -10,8 +10,10 @@ import androidx.lifecycle.LifecycleOwner import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.xxlabs.messenger.backup.auth.CloudAuthentication -import io.xxlabs.messenger.backup.model.BackupOption +import io.xxlabs.messenger.backup.cloud.CloudAuthentication +import io.xxlabs.messenger.backup.data.BackupSource +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialogUI +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialog import io.xxlabs.messenger.databinding.FragmentBackupSettingsBinding import io.xxlabs.messenger.databinding.ListItemBackupOptionBinding import io.xxlabs.messenger.di.utils.Injectable @@ -94,9 +96,9 @@ class BackupSettingsFragment : Fragment(), Injectable { } } - ui.navigateToDetail.observe(viewLifecycleOwner) { option -> - option?.let { - onNavigateToDetail(option) + ui.navigateToDetail.observe(viewLifecycleOwner) { backup -> + backup?.let { + onNavigateToDetail(backup) } } } @@ -105,8 +107,8 @@ class BackupSettingsFragment : Fragment(), Injectable { ui.onInfoDialogHandled() } - private fun showSetPasswordDialog(dialogUI: EditTextTwoButtonDialogUI) { - EditTextTwoButtonInfoDialog.newInstance(dialogUI) + private fun showSetPasswordDialog(dialogUI: TextInputDialogUI) { + TextInputDialog.newInstance(dialogUI) .show(childFragmentManager, null) ui.onPasswordPromptHandled() } @@ -116,9 +118,9 @@ class BackupSettingsFragment : Fragment(), Injectable { ui.onErrorHandled() } - private fun onNavigateToDetail(backup: BackupOption) { + private fun onNavigateToDetail(source: BackupSource) { val directions = BackupSettingsFragmentDirections - .actionBackupSettingsToBackupDetail(backup) + .actionBackupSettingsToBackupDetail(source) findNavController().navigate(directions) ui.onNavigationHandled() } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsUI.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsUI.kt similarity index 64% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsUI.kt rename to app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsUI.kt index cb13d04fedf2931e21438580c7df3f2a5eea569d..16f598f608dbd092c2439290326678a171c840fb 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsUI.kt @@ -1,21 +1,21 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import android.text.Spanned import androidx.lifecycle.LiveData -import io.xxlabs.messenger.backup.model.BackupOption -import io.xxlabs.messenger.backup.model.BackupSettings -import io.xxlabs.messenger.backup.ui.list.LocationOption +import io.xxlabs.messenger.backup.data.BackupSource +import io.xxlabs.messenger.backup.data.backup.BackupSettings +import io.xxlabs.messenger.backup.model.AccountBackup interface BackupSettingsUI : BackupPasswordUI { val description: Spanned // for clickable info button - val backup: BackupOption? + val backup: AccountBackup? val settings: LiveData<BackupSettings> val backupInProgress: LiveData<Boolean> } interface BackupSettingsController: BackupSettingsUI { val locations: List<SettingsOption> - val navigateToDetail: LiveData<BackupOption?> + val navigateToDetail: LiveData<BackupSource?> val showInfoDialog: LiveData<Boolean> val backupError: LiveData<String?> fun onInfoDialogHandled() diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsViewModel.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsViewModel.kt similarity index 72% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsViewModel.kt rename to app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsViewModel.kt index dc3c7f474218292d19a78a0cc092d4c8d6d8e850..2a60f2feb8cc1a3b34fe321fc3a11c7df90fbfe1 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupSettingsViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupSettingsViewModel.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import android.text.SpannableString import android.text.Spanned @@ -7,36 +7,43 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.auth.CloudAuthentication -import io.xxlabs.messenger.backup.data.BackupDataSource +import io.xxlabs.messenger.backup.cloud.AuthResultCallback +import io.xxlabs.messenger.backup.cloud.CloudAuthentication +import io.xxlabs.messenger.backup.data.BackupSource +import io.xxlabs.messenger.backup.data.backup.BackupManager +import io.xxlabs.messenger.backup.data.backup.BackupOption +import io.xxlabs.messenger.backup.data.backup.BackupSettings import io.xxlabs.messenger.backup.model.* import io.xxlabs.messenger.backup.ui.list.LocationOption import io.xxlabs.messenger.support.appContext class BackupSettingsViewModel @AssistedInject constructor( - dataSource: BackupDataSource<BackupOption>, + backupManager: BackupManager, @Assisted private val cloudAuthSource: CloudAuthentication, -) : BackupViewModel(dataSource), BackupSettingsController { +) : BackupViewModel(backupManager), BackupSettingsController { override val description: Spanned = getSpannedDescription() - override val backup: BackupOption? get() = dataSource.getActiveOption() - override val settings: LiveData<BackupSettings> by dataSource::settings + override val backup: AccountBackup? get() = backupManager.getActiveBackupOption() + override val settings: LiveData<BackupSettings> = backupManager.settings.asLiveData() override val backupInProgress: LiveData<Boolean> = isBackupRunning() override val locations: List<SettingsOption> - get() = dataSource.locations.map { - BackupSettingsOption( - it, - ::onLocationSelected, - ::onEnableToggled, - backup?.location == it, - isEnabled - ) - } + get() = backupManager.locations.map { + backupLocationsMap[it.location] = it + BackupSettingsOption( + it.location, + ::onLocationSelected, + ::onEnableToggled, + backup?.location == it.location, + isEnabled + ) + } + + private val backupLocationsMap: MutableMap<BackupLocation, AccountBackup> = mutableMapOf() - override val navigateToDetail: LiveData<BackupOption?> get() = _navigateToDetail - private val _navigateToDetail = MutableLiveData<BackupOption?>(null) + override val navigateToDetail: LiveData<BackupSource?> get() = _navigateToDetail + private val _navigateToDetail = MutableLiveData<BackupSource?>(null) private val _showInfoDialog = MutableLiveData(false) override val showInfoDialog: LiveData<Boolean> = @@ -56,7 +63,7 @@ class BackupSettingsViewModel @AssistedInject constructor( } override fun getBackupOption(): BackupOption? { - return backup + return backup as? BackupOption } override fun onInfoDialogHandled() { @@ -96,7 +103,11 @@ class BackupSettingsViewModel @AssistedInject constructor( } private fun navigateToDetail(backupLocation: BackupLocation) { - _navigateToDetail.value = dataSource.getBackupDetails(backupLocation) + backupLocationsMap[backupLocation]?.let { backup -> + backupManager.getSourceFor(backup)?.let { source -> + _navigateToDetail.value = source + } + } } companion object { diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupViewModel.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupViewModel.kt similarity index 76% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupViewModel.kt rename to app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupViewModel.kt index 7198317dfa0b759b28fb880403e076828ea788fc..68f2f1883adcfe08e70ad95807338bf3b73a90a0 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/BackupViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/backup/BackupViewModel.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import android.text.Editable import androidx.lifecycle.LiveData @@ -6,18 +6,20 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.data.BackupDataSource -import io.xxlabs.messenger.backup.data.BackupTaskListener -import io.xxlabs.messenger.backup.model.BackupOption +import io.xxlabs.messenger.backup.data.backup.BackupManager +import io.xxlabs.messenger.backup.data.backup.BackupOption +import io.xxlabs.messenger.backup.data.backup.BackupTaskListener +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialogUI import io.xxlabs.messenger.support.appContext -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -abstract class BackupViewModel(val dataSource: BackupDataSource<BackupOption>) - : ViewModel(), BackupPasswordUI, BackupTaskListener { +abstract class BackupViewModel( + val backupManager: BackupManager +) : ViewModel(), BackupPasswordUI, BackupTaskListener { abstract fun getBackupOption(): BackupOption? @@ -29,14 +31,14 @@ abstract class BackupViewModel(val dataSource: BackupDataSource<BackupOption>) override val isEnabled: LiveData<Boolean> by ::_isEnabled private val _isEnabled by lazy { MutableLiveData(getBackupOption()?.isEnabled() ?: false) } - override val showSetPasswordPrompt: LiveData<EditTextTwoButtonDialogUI?> by ::_showSetPasswordPrompt - private val _showSetPasswordPrompt = MutableLiveData<EditTextTwoButtonDialogUI?>(null) + override val showSetPasswordPrompt: LiveData<TextInputDialogUI?> by ::_showSetPasswordPrompt + private val _showSetPasswordPrompt = MutableLiveData<TextInputDialogUI?>(null) private val passwordInputError = MutableLiveData<String?>(null) private val passwordPromptPositiveButtonEnabled = MutableLiveData(false) - private val passwordPromptUI: EditTextTwoButtonDialogUI by lazy { - EditTextTwoButtonDialogUI.create( + private val passwordPromptUI: TextInputDialogUI by lazy { + TextInputDialogUI.create( BackupPassword.MAX_LENGTH, R.string.backup_restore_password_prompt_hint, passwordInputError, @@ -57,7 +59,13 @@ abstract class BackupViewModel(val dataSource: BackupDataSource<BackupOption>) } init { - dataSource.subscribe(getBackupTaskListener()) + backupManager.subscribe(getBackupTaskListener()) + } + + override fun backupNow() { + getBackupOption()?.let { + backupManager.backupNow(it) + } } private fun getBackupTaskListener() = this @@ -96,7 +104,7 @@ abstract class BackupViewModel(val dataSource: BackupDataSource<BackupOption>) viewModelScope.launch { withContext(Dispatchers.IO) { - dataSource.enableBackup(it, backupPassword) + backupManager.enableBackup(it, backupPassword) } } } @@ -120,7 +128,7 @@ abstract class BackupViewModel(val dataSource: BackupDataSource<BackupOption>) private fun disableBackup() { getBackupOption()?.let { - dataSource.disableBackup(it) + backupManager.disableBackup(it) } _isEnabled.value = false } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupListFragment.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupListFragment.kt index 520c628f7817cb0ffbf55f3df89f155b1d66aa20..a63397d621da40a762dc7da254328ca3a9a276b8 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupListFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupListFragment.kt @@ -2,13 +2,13 @@ package io.xxlabs.messenger.backup.ui.list import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController -import io.xxlabs.messenger.backup.model.BackupOption +import io.xxlabs.messenger.backup.data.BackupSource import javax.inject.Inject /** * Lists the backup locations to save to. */ -class BackupListFragment : BackupLocationsFragment<BackupOption>() { +class BackupListFragment : BackupLocationsFragment() { @Inject lateinit var backupListViewModelFactory: BackupListViewModelFactory @@ -19,9 +19,9 @@ class BackupListFragment : BackupLocationsFragment<BackupOption>() { ) } - override fun navigateToDetail(backup: BackupOption) { + override fun navigateToDetail(source: BackupSource) { val directions = BackupListFragmentDirections - .actionBackupListToBackupDetail(backup) + .actionBackupListToBackupDetail(source) findNavController().navigate(directions) } } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupListViewModel.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupListViewModel.kt index 75ca71e18c05b3ab4adf4c553ffb72bd000bf825..161126bb530382e127dbc806d93cd6c66b020662 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupListViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupListViewModel.kt @@ -10,15 +10,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.auth.CloudAuthentication -import io.xxlabs.messenger.backup.data.BackupDataSource -import io.xxlabs.messenger.backup.model.BackupOption +import io.xxlabs.messenger.backup.cloud.CloudAuthentication +import io.xxlabs.messenger.backup.data.backup.BackupManager import io.xxlabs.messenger.support.appContext class BackupListViewModel @AssistedInject constructor( - dataSource: BackupDataSource<BackupOption>, + backupManager: BackupManager, @Assisted cloudAuthSource: CloudAuthentication -) : BackupLocationsViewModel<BackupOption>(dataSource, cloudAuthSource) { +) : BackupLocationsViewModel(backupManager, cloudAuthSource) { override val backupLocationsTitle: Spanned = getSpannableTitle() override val backupLocationsDescription: Spanned = getSpannableDescription() diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsFragment.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsFragment.kt index 40e88fbe9df07b7039ee54565ef1314c91dc9a08..bf21204cd2a55f2668c5de6cdfa46fd7fec021c9 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsFragment.kt @@ -9,29 +9,29 @@ import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.auth.CloudAuthentication -import io.xxlabs.messenger.backup.model.AccountBackup +import io.xxlabs.messenger.backup.cloud.CloudAuthentication +import io.xxlabs.messenger.backup.data.BackupSource import io.xxlabs.messenger.databinding.FragmentBackupLocationsBinding import io.xxlabs.messenger.databinding.ListItemBackupLocationBinding import io.xxlabs.messenger.di.utils.Injectable import io.xxlabs.messenger.support.view.SnackBarActivity -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialog -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialog +import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI /** * Lists cloud storage services to choose as a backup name. */ -abstract class BackupLocationsFragment<T: AccountBackup> : Fragment(), Injectable { +abstract class BackupLocationsFragment : Fragment(), Injectable { /* ViewModels */ - abstract val backupViewModel: BackupLocationsViewModel<T> + abstract val backupViewModel: BackupLocationsViewModel protected lateinit var cloudAuthentication: CloudAuthentication /* UI */ private lateinit var binding: FragmentBackupLocationsBinding - private val ui: BackupLocationsController<T> by lazy { backupViewModel } + private val ui: BackupLocationsController by lazy { backupViewModel } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -73,9 +73,9 @@ abstract class BackupLocationsFragment<T: AccountBackup> : Fragment(), Injectabl } private fun observeUI() { - ui.navigateToDetail.observe(viewLifecycleOwner) { backup -> - backup?.let { - navigateToDetail(backup) + ui.navigateToDetail.observe(viewLifecycleOwner) { source -> + source?.let { + navigateToDetail(source) ui.onNavigationHandled() } } @@ -95,7 +95,7 @@ abstract class BackupLocationsFragment<T: AccountBackup> : Fragment(), Injectabl } } - protected abstract fun navigateToDetail(backup: T) + protected abstract fun navigateToDetail(source: BackupSource) private fun showConsentDialog(dialogUi: TwoButtonInfoDialogUI) { TwoButtonInfoDialog.newInstance(dialogUi) diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsUI.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsUI.kt index 9f4372748b69e7d0312c3db9fd9b6a0c039fada3..8b489bfbac8332a08b140bb803f106b5f4f7949b 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsUI.kt @@ -2,10 +2,8 @@ package io.xxlabs.messenger.backup.ui.list import android.text.Spanned import androidx.lifecycle.LiveData -import io.xxlabs.messenger.backup.model.AccountBackup -import io.xxlabs.messenger.support.dialog.confirm.ConfirmDialogUI -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialog -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialogUI +import io.xxlabs.messenger.backup.data.BackupSource +import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI interface BackupLocationsUI { val backupLocationsTitle: Spanned @@ -13,9 +11,9 @@ interface BackupLocationsUI { val isLoading: LiveData<Boolean> } -interface BackupLocationsController<T: AccountBackup>: BackupLocationsUI { +interface BackupLocationsController : BackupLocationsUI { val locations: List<LocationOption> - val navigateToDetail: LiveData<T?> + val navigateToDetail: LiveData<BackupSource?> val authLaunchConsentDialog: LiveData<TwoButtonInfoDialogUI?> val backupError: LiveData<String?> fun onNavigationHandled() diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsViewModel.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsViewModel.kt index fd34eef0375e5bbf96d03bd7d431ec92f1906628..2bf9ddb847c637ca6071f0e01c474b7dc02caf69 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/BackupLocationsViewModel.kt @@ -2,29 +2,35 @@ package io.xxlabs.messenger.backup.ui.list import androidx.lifecycle.* import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.auth.CloudAuthentication -import io.xxlabs.messenger.backup.data.BackupDataSource +import io.xxlabs.messenger.backup.cloud.CloudAuthentication import io.xxlabs.messenger.backup.model.AccountBackup import io.xxlabs.messenger.backup.model.BackupLocation -import io.xxlabs.messenger.backup.model.AuthResultCallback +import io.xxlabs.messenger.backup.cloud.AuthResultCallback +import io.xxlabs.messenger.backup.data.AccountBackupDataSource +import io.xxlabs.messenger.backup.data.BackupSource import io.xxlabs.messenger.support.appContext -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI -abstract class BackupLocationsViewModel<T: AccountBackup>( - private val dataSource: BackupDataSource<T>, +abstract class BackupLocationsViewModel( + private val dataSource: AccountBackupDataSource, private val cloudAuthSource: CloudAuthentication, -) : ViewModel(), BackupLocationsController<T> { +) : ViewModel(), BackupLocationsController { private var backupLocation: BackupLocation? = null /* UI */ + private val backupLocationsMap: MutableMap<BackupLocation, AccountBackup> = mutableMapOf() + override val locations: List<LocationOption> = - dataSource.locations.map { BackupLocationOption(it, ::onLocationSelected) } + dataSource.locations.map { + backupLocationsMap[it.location] = it + BackupLocationOption(it.location, ::onLocationSelected) + } - override val navigateToDetail: LiveData<T?> by ::_navigateToDetail - private val _navigateToDetail = MutableLiveData<T?>(null) + override val navigateToDetail: LiveData<BackupSource?> by ::_navigateToDetail + private val _navigateToDetail = MutableLiveData<BackupSource?>(null) override val authLaunchConsentDialog: LiveData<TwoButtonInfoDialogUI?> by ::_authLaunchConsentDialog @@ -83,7 +89,7 @@ abstract class BackupLocationsViewModel<T: AccountBackup>( _authLaunchConsentDialog.value = null } - protected fun signIn(backupLocation: BackupLocation) { + private fun signIn(backupLocation: BackupLocation) { setLoading(true) val authHandler = backupLocation.createAuthHandler( object : AuthResultCallback { @@ -109,11 +115,15 @@ abstract class BackupLocationsViewModel<T: AccountBackup>( } protected fun navigateToDetail(backupLocation: BackupLocation) { - _navigateToDetail.value = getAccountBackup(backupLocation) + backupLocationsMap[backupLocation]?.let { backup -> + dataSource.getSourceFor(backup)?.let { source -> + _navigateToDetail.value = source + } + } } - protected fun getAccountBackup(backupLocation: BackupLocation): T = - dataSource.setLocation(backupLocation) + protected fun getAccountBackup(backupLocation: BackupLocation): AccountBackup? = + backupLocationsMap[backupLocation] } private class AuthLaunchConsentHandler private constructor() { diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/RestoreListFragment.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/RestoreListFragment.kt index 9332366ce744bc9cb8a01be38819fe31187fed68..718ddae94ce339c5dec563ec5c00484a3bdd9664 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/RestoreListFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/RestoreListFragment.kt @@ -5,14 +5,14 @@ import android.view.View import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.model.RestoreOption -import io.xxlabs.messenger.ui.ConfirmDialogLauncher +import io.xxlabs.messenger.backup.data.BackupSource +import io.xxlabs.messenger.ui.dialog.warning.showConfirmDialog import javax.inject.Inject /** * Lists the backup locations to restore from. */ -class RestoreListFragment : BackupLocationsFragment<RestoreOption>() { +class RestoreListFragment : BackupLocationsFragment() { /* ViewModels */ @@ -27,10 +27,6 @@ class RestoreListFragment : BackupLocationsFragment<RestoreOption>() { /* UI */ - private val warningLauncher: ConfirmDialogLauncher by lazy { - ConfirmDialogLauncher(requireActivity().supportFragmentManager) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) showMultiDeviceWarning() @@ -49,28 +45,9 @@ class RestoreListFragment : BackupLocationsFragment<RestoreOption>() { private fun onConfirmButtonClicked() {} private fun onConfirmDialogDismissed() {} - private fun showConfirmDialog( - title: Int, - body: Int, - button: Int, - action: () -> Unit, - onDismiss: () -> Unit = {} - ) { - warningLauncher.showConfirmDialog(title, body, button, action, onDismiss) - } - - override fun onStart() { - super.onStart() - observeUI() - } - - private fun observeUI() { - - } - - override fun navigateToDetail(backup: RestoreOption) { + override fun navigateToDetail(source: BackupSource) { val directions = RestoreListFragmentDirections - .actionRestoreListToRestoreDetail(backup) + .actionRestoreListToRestoreDetail(source) findNavController().navigate(directions) } } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/RestoreListViewModel.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/RestoreListViewModel.kt index afba41f6fa8c5cc287b11f6be0f91c4c57229132..e36b0143935d733f3a55c91721b14fde28bf0582 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/list/RestoreListViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/list/RestoreListViewModel.kt @@ -9,16 +9,16 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.auth.CloudAuthentication -import io.xxlabs.messenger.backup.data.BackupDataSource +import io.xxlabs.messenger.backup.cloud.CloudAuthentication +import io.xxlabs.messenger.backup.data.restore.RestoreManager +import io.xxlabs.messenger.backup.model.AccountBackup import io.xxlabs.messenger.backup.model.BackupLocation -import io.xxlabs.messenger.backup.model.RestoreOption import io.xxlabs.messenger.support.appContext class RestoreListViewModel @AssistedInject constructor( - dataSource: BackupDataSource<RestoreOption>, + restoreManager: RestoreManager, @Assisted cloudAuthSource: CloudAuthentication, -) : BackupLocationsViewModel<RestoreOption>(dataSource, cloudAuthSource) { +) : BackupLocationsViewModel(restoreManager, cloudAuthSource) { override val backupLocationsTitle: Spanned = getSpannableTitle() override val backupLocationsDescription: Spanned = getSpannableDescription() @@ -42,14 +42,14 @@ class RestoreListViewModel @AssistedInject constructor( } override fun onAuthSuccess(backupLocation: BackupLocation) { - if (getAccountBackup(backupLocation).hasBackup()) { + if (getAccountBackup(backupLocation)?.hasBackup() == true) { navigateToDetail(backupLocation) } else { setError(appContext().getString(R.string.backup_restore_error_no_backup_found)) } } - private fun RestoreOption.hasBackup(): Boolean = + private fun AccountBackup.hasBackup(): Boolean = lastBackup.value?.run { sizeBytes > 0 } ?: false diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailFragment.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailFragment.kt index fd46d3dddbf62c2d9fb488eb109579481b8056da..82bab8ddac4555e4ea46a5af01bd5415c8c75b3f 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailFragment.kt @@ -9,15 +9,14 @@ import androidx.databinding.DataBindingUtil import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.ui.save.EditTextTwoButtonDialogUI -import io.xxlabs.messenger.backup.ui.save.EditTextTwoButtonInfoDialog +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialogUI +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialog import io.xxlabs.messenger.databinding.FragmentRestoreDetailBinding import io.xxlabs.messenger.di.utils.Injectable -import io.xxlabs.messenger.support.extensions.toBase64String import io.xxlabs.messenger.support.extensions.toast -import io.xxlabs.messenger.ui.ConfirmDialogLauncher import io.xxlabs.messenger.ui.base.BaseKeystoreActivity import io.xxlabs.messenger.ui.intro.registration.success.RegistrationStep +import io.xxlabs.messenger.ui.dialog.warning.showConfirmDialog import javax.inject.Inject class RestoreDetailFragment : Fragment(), Injectable { @@ -29,7 +28,7 @@ class RestoreDetailFragment : Fragment(), Injectable { private val restoreViewModel: RestoreDetailViewModel by viewModels { RestoreDetailViewModel.provideFactory( viewModelFactory, - RestoreDetailFragmentArgs.fromBundle(requireArguments()).restoreOption, + RestoreDetailFragmentArgs.fromBundle(requireArguments()).source, (requireActivity() as BaseKeystoreActivity).rsaDecryptPwd() ) } @@ -38,9 +37,6 @@ class RestoreDetailFragment : Fragment(), Injectable { private lateinit var binding: FragmentRestoreDetailBinding private val ui: RestoreDetailController by lazy { restoreViewModel } - private val warningLauncher: ConfirmDialogLauncher by lazy { - ConfirmDialogLauncher(requireActivity().supportFragmentManager) - } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -65,16 +61,6 @@ class RestoreDetailFragment : Fragment(), Injectable { private fun onConfirmButtonClicked() {} private fun onConfirmDialogDismissed() {} - private fun showConfirmDialog( - title: Int, - body: Int, - button: Int, - action: () -> Unit, - onDismiss: () -> Unit = {} - ) { - warningLauncher.showConfirmDialog(title, body, button, action, onDismiss) - } - private fun showMultiDeviceWarning() { showConfirmDialog( R.string.backup_restore_multidevice_warning_dialog_title, @@ -123,8 +109,8 @@ class RestoreDetailFragment : Fragment(), Injectable { ).commitAllowingStateLoss() } - private fun showSetPasswordDialog(dialogUI: EditTextTwoButtonDialogUI) { - EditTextTwoButtonInfoDialog.newInstance(dialogUI) + private fun showSetPasswordDialog(dialogUI: TextInputDialogUI) { + TextInputDialog.newInstance(dialogUI) .show(childFragmentManager, null) ui.onPasswordPromptHandled() } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailUI.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailUI.kt index b6f33e5f7e6436d40dea6fb586459572f84c30ec..c1cd8fc42470b4a5d1ee3f55b607e57036014c57 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailUI.kt @@ -1,19 +1,18 @@ package io.xxlabs.messenger.backup.ui.restore import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations -import io.xxlabs.messenger.backup.model.RestoreOption -import io.xxlabs.messenger.backup.ui.save.EditTextTwoButtonDialogUI +import io.xxlabs.messenger.backup.data.restore.RestoreLog +import io.xxlabs.messenger.backup.model.AccountBackup +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialogUI import java.io.Serializable interface RestorePasswordUI { - val showEnterPasswordPrompt: LiveData<EditTextTwoButtonDialogUI?> + val showEnterPasswordPrompt: LiveData<TextInputDialogUI?> fun onPasswordPromptHandled() } interface RestoreDetailUI : RestorePasswordUI { - val backup: RestoreOption + val backup: AccountBackup val state: LiveData<RestoreState> val isLoading: LiveData<Boolean> } @@ -41,7 +40,8 @@ interface RestoreReady : RestoreState { } interface RestoreStarted: RestoreState { - val restore: RestoreOption + val accountBackup: AccountBackup + val restoreLog: RestoreLog? } interface RestoreSuccess : RestoreState { diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailViewModel.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailViewModel.kt index ff6f69b3ec53286f63f9bdf46a3fb8f38bb1245e..0860884c0308ae6c255db604297f7e9136605d91 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreDetailViewModel.kt @@ -6,24 +6,27 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.data.BackupDataSource -import io.xxlabs.messenger.backup.model.RestoreEnvironment -import io.xxlabs.messenger.backup.model.RestoreOption -import io.xxlabs.messenger.backup.ui.save.BackupPassword -import io.xxlabs.messenger.backup.ui.save.EditTextTwoButtonDialogUI +import io.xxlabs.messenger.backup.data.BackupSource +import io.xxlabs.messenger.backup.data.restore.RestoreEnvironment +import io.xxlabs.messenger.backup.data.restore.RestoreLog +import io.xxlabs.messenger.backup.data.restore.RestoreManager +import io.xxlabs.messenger.backup.model.AccountBackup +import io.xxlabs.messenger.backup.ui.backup.BackupPassword +import io.xxlabs.messenger.ui.dialog.textinput.TextInputDialogUI import io.xxlabs.messenger.bindings.wrapper.bindings.BindingsWrapperBindings import io.xxlabs.messenger.support.appContext -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI import kotlinx.coroutines.* -import java.lang.IllegalArgumentException class RestoreDetailViewModel @AssistedInject constructor( - private val dataSource: BackupDataSource<RestoreOption>, - @Assisted override val backup: RestoreOption, + private val restoreManager: RestoreManager, + @Assisted private val source: BackupSource, @Assisted private val restorePassword: ByteArray, ): ViewModel(), RestoreDetailController { + override val backup: AccountBackup get() = restoreManager.getBackupFrom(source) + private val exceptionHandler = CoroutineExceptionHandler { _, exception -> exception.message?.let { val errorMessage = @@ -31,7 +34,7 @@ class RestoreDetailViewModel @AssistedInject constructor( else it _restoreError.postValue(errorMessage) } - backup.cancelRestore() + restoreManager.cancelRestore(backup) } override val restoreComplete: LiveData<Boolean> by ::_restoreComplete @@ -63,7 +66,9 @@ class RestoreDetailViewModel @AssistedInject constructor( ) private val startedState: RestoreStarted = object : RestoreStarted { - override val restore: RestoreOption = backup + override val accountBackup: AccountBackup = backup + override val restoreLog: RestoreLog? + get() = restoreManager.getRestoreLog(backup) } private val success: RestoreSuccess get() = @@ -75,14 +80,14 @@ class RestoreDetailViewModel @AssistedInject constructor( _isLoading.value = loading } - override val showEnterPasswordPrompt: LiveData<EditTextTwoButtonDialogUI?> by ::_showEnterPasswordPrompt - private val _showEnterPasswordPrompt = MutableLiveData<EditTextTwoButtonDialogUI?>(null) + override val showEnterPasswordPrompt: LiveData<TextInputDialogUI?> by ::_showEnterPasswordPrompt + private val _showEnterPasswordPrompt = MutableLiveData<TextInputDialogUI?>(null) private val passwordInputError = MutableLiveData<String?>(null) private val passwordPromptPositiveButtonEnabled = MutableLiveData(true) - private val passwordPromptUI: EditTextTwoButtonDialogUI by lazy { - EditTextTwoButtonDialogUI.create( + private val passwordPromptUI: TextInputDialogUI by lazy { + TextInputDialogUI.create( BackupPassword.MAX_LENGTH, R.string.backup_restore_password_restore_hint, passwordInputError, @@ -122,7 +127,7 @@ class RestoreDetailViewModel @AssistedInject constructor( setLoading(true) restoreTask?.let { return@let } restoreTask = viewModelScope.launch(exceptionHandler) { - backup.restore(restoreEnvironment) + restoreManager.restore(backup, restoreEnvironment) } } @@ -147,11 +152,11 @@ class RestoreDetailViewModel @AssistedInject constructor( companion object { fun provideFactory( assistedFactory: BackupFoundViewModelFactory, - backup: RestoreOption, + source: BackupSource, restorePassword: ByteArray ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { - return assistedFactory.create(backup, restorePassword) as T + return assistedFactory.create(source, restorePassword) as T } } } @@ -159,5 +164,5 @@ class RestoreDetailViewModel @AssistedInject constructor( @AssistedFactory interface BackupFoundViewModelFactory { - fun create(backup: RestoreOption, restorePassword: ByteArray): RestoreDetailViewModel + fun create(source: BackupSource, restorePassword: ByteArray): RestoreDetailViewModel } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreStateFragment.kt b/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreStateFragment.kt index 11b38080a52ad1a00b2b8a6c3da2fed7164f34b0..1b6117c72da247bf3ffa7a7fa5bf6223103c578c 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreStateFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/backup/ui/restore/RestoreStateFragment.kt @@ -6,14 +6,10 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.ViewDataBinding import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import io.xxlabs.messenger.R -import io.xxlabs.messenger.backup.data.RestoreLog +import io.xxlabs.messenger.backup.data.restore.RestoreLog import io.xxlabs.messenger.databinding.FragmentRestoreReadyBinding import io.xxlabs.messenger.databinding.FragmentRestoreStartedBinding import io.xxlabs.messenger.databinding.FragmentRestoreSuccessBinding -import kotlinx.android.synthetic.main.list_item_event.view.* open class RestoreStateFragment : Fragment() { @@ -61,8 +57,8 @@ class RestoreStartedFragment : RestoreStateFragment() { private val ui: FragmentRestoreStartedBinding by lazy { binding as FragmentRestoreStartedBinding } - private val restoreLog: RestoreLog by lazy { - (state as RestoreStarted).restore.restoreLog + private val restoreLog: RestoreLog? by lazy { + (state as RestoreStarted).restoreLog } override fun onStart() { @@ -71,7 +67,7 @@ class RestoreStartedFragment : RestoreStateFragment() { } private fun observeProgress() { - restoreLog.data.observe(viewLifecycleOwner) { events -> + restoreLog?.data?.observe(viewLifecycleOwner) { events -> if (events.isNotEmpty()) ui.restoreProgressText.text = events[events.size-1] } } diff --git a/app/src/main/java/io/xxlabs/messenger/bindings/wrapper/bindings/BindingsWrapperBindings.kt b/app/src/main/java/io/xxlabs/messenger/bindings/wrapper/bindings/BindingsWrapperBindings.kt index 2dade24eabcad8ef6e407f298204148a41195fd5..c8d02c31e5f44bd4a52323b98dc991469654e8e0 100644 --- a/app/src/main/java/io/xxlabs/messenger/bindings/wrapper/bindings/BindingsWrapperBindings.kt +++ b/app/src/main/java/io/xxlabs/messenger/bindings/wrapper/bindings/BindingsWrapperBindings.kt @@ -24,6 +24,7 @@ import java.io.File import java.lang.UnsupportedOperationException private val devUserDiscoveryIp = "18.198.117.203:11420".encodeToByteArray() +private const val NDF_MAX_RETRIES = 2 class BindingsWrapperBindings { @@ -33,8 +34,10 @@ class BindingsWrapperBindings { private const val NDF_URL_RELEASE = "https://elixxir-bins.s3.us-west-1.amazonaws.com/ndf/release.json" - override fun getNdf(): String { - return when (BuildConfig.ENVIRONMENT) { + override fun getNdf(): String = recursiveGetNdf() + + private fun recursiveGetNdf(retries: Int = 0): String { + val ndf = when (BuildConfig.ENVIRONMENT) { Environment.MAIN_NET -> { downloadAndVerifySignedNdfWithUrl( NDF_URL_MAINNET, @@ -49,6 +52,12 @@ class BindingsWrapperBindings { } else -> getLocalNdf() } + + return if (ndf.isEmpty() && retries <= NDF_MAX_RETRIES) { + recursiveGetNdf(retries + 1) + } else { + ndf + } } private fun certificateFor(environment: Environment): String { diff --git a/app/src/main/java/io/xxlabs/messenger/data/data/Country.kt b/app/src/main/java/io/xxlabs/messenger/data/data/Country.kt index 135ddafe1999ac286c23d47bf4e9d0171c4b829e..5c61fc381819f48bf742a04cddf06486c8d767cf 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/data/Country.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/data/Country.kt @@ -107,6 +107,6 @@ data class Country( } } - private const val countriesJson = "[ { \"name\": \"Afghanistan\", \"dial_code\": \"+93\", \"code\": \"AF\", \"flag\": \"🇦🇫\" }, { \"name\": \"Albania\", \"dial_code\": \"+355\", \"code\": \"AL\", \"flag\": \"🇦🇱\" }, { \"name\": \"Algeria\", \"dial_code\": \"+213\", \"code\": \"DZ\", \"flag\": \"🇩🇿\" }, { \"name\": \"AmericanSamoa\", \"dial_code\": \"+1684\", \"code\": \"AS\", \"flag\": \"🇦🇸\" }, { \"name\": \"Andorra\", \"dial_code\": \"+376\", \"code\": \"AD\", \"flag\": \"🇦🇩\" }, { \"name\": \"Angola\", \"dial_code\": \"+244\", \"code\": \"AO\", \"flag\": \"🇦🇴\" }, { \"name\": \"Anguilla\", \"dial_code\": \"+1264\", \"code\": \"AI\", \"flag\": \"🇦🇮\" }, { \"name\": \"Antarctica\", \"dial_code\": \"+672\", \"code\": \"AQ\", \"flag\": \"🇦🇶\" }, { \"name\": \"Antigua and Barbuda\", \"dial_code\": \"+1268\", \"code\": \"AG\", \"flag\": \"🇦🇬\" }, { \"name\": \"Argentina\", \"dial_code\": \"+54\", \"code\": \"AR\", \"flag\": \"🇦🇷\" }, { \"name\": \"Armenia\", \"dial_code\": \"+374\", \"code\": \"AM\", \"flag\": \"🇦🇲\" }, { \"name\": \"Aruba\", \"dial_code\": \"+297\", \"code\": \"AW\", \"flag\": \"🇦🇼\" }, { \"name\": \"Australia\", \"dial_code\": \"+61\", \"code\": \"AU\", \"preferred\": true, \"flag\": \"🇦🇺\" }, { \"name\": \"Austria\", \"dial_code\": \"+43\", \"code\": \"AT\", \"flag\": \"🇦🇹\" }, { \"name\": \"Azerbaijan\", \"dial_code\": \"+994\", \"code\": \"AZ\", \"flag\": \"🇦🇿\" }, { \"name\": \"Bahamas\", \"dial_code\": \"+1242\", \"code\": \"BS\", \"flag\": \"🇧🇸\" }, { \"name\": \"Bahrain\", \"dial_code\": \"+973\", \"code\": \"BH\", \"flag\": \"🇧🇭\" }, { \"name\": \"Bangladesh\", \"dial_code\": \"+880\", \"code\": \"BD\", \"flag\": \"🇧🇩\" }, { \"name\": \"Barbados\", \"dial_code\": \"+1246\", \"code\": \"BB\", \"flag\": \"🇧🇧\" }, { \"name\": \"Belarus\", \"dial_code\": \"+375\", \"code\": \"BY\", \"flag\": \"🇧🇾\" }, { \"name\": \"Belgium\", \"dial_code\": \"+32\", \"code\": \"BE\", \"flag\": \"🇧🇪\" }, { \"name\": \"Belize\", \"dial_code\": \"+501\", \"code\": \"BZ\", \"flag\": \"🇧🇿\" }, { \"name\": \"Benin\", \"dial_code\": \"+229\", \"code\": \"BJ\", \"flag\": \"🇧🇯\" }, { \"name\": \"Bermuda\", \"dial_code\": \"+1441\", \"code\": \"BM\", \"flag\": \"🇧🇲\" }, { \"name\": \"Bhutan\", \"dial_code\": \"+975\", \"code\": \"BT\", \"flag\": \"🇧🇹\" }, { \"name\": \"Bolivia, Plurinational State of\", \"dial_code\": \"+591\", \"code\": \"BO\", \"flag\": \"🇧🇴\" }, { \"name\": \"Bosnia and Herzegovina\", \"dial_code\": \"+387\", \"code\": \"BA\", \"flag\": \"🇧🇦\" }, { \"name\": \"Botswana\", \"dial_code\": \"+267\", \"code\": \"BW\", \"flag\": \"🇧🇼\" }, { \"name\": \"Brazil\", \"dial_code\": \"+55\", \"code\": \"BR\", \"flag\": \"🇧🇷\" }, { \"name\": \"British Indian Ocean Territory\", \"dial_code\": \"+246\", \"code\": \"IO\", \"flag\": \"🇮🇴\" }, { \"name\": \"Brunei Darussalam\", \"dial_code\": \"+673\", \"code\": \"BN\", \"flag\": \"🇧🇳\" }, { \"name\": \"Bulgaria\", \"dial_code\": \"+359\", \"code\": \"BG\", \"flag\": \"🇧🇬\" }, { \"name\": \"Burkina Faso\", \"dial_code\": \"+226\", \"code\": \"BF\", \"flag\": \"🇧🇫\" }, { \"name\": \"Burundi\", \"dial_code\": \"+257\", \"code\": \"BI\", \"flag\": \"🇧🇮\" }, { \"name\": \"Cambodia\", \"dial_code\": \"+855\", \"code\": \"KH\", \"flag\": \"🇰🇭\" }, { \"name\": \"Cameroon\", \"dial_code\": \"+237\", \"code\": \"CM\", \"flag\": \"🇨🇲\" }, { \"name\": \"Canada\", \"dial_code\": \"+1\", \"code\": \"CA\", \"flag\": \"🇨🇦\" }, { \"name\": \"Cape Verde\", \"dial_code\": \"+238\", \"code\": \"CV\", \"flag\": \"🇨🇻\" }, { \"name\": \"Cayman Islands\", \"dial_code\": \"+345\", \"code\": \"KY\", \"flag\": \"🇰🇾\" }, { \"name\": \"Central African Republic\", \"dial_code\": \"+236\", \"code\": \"CF\", \"flag\": \"🇨🇫\" }, { \"name\": \"Chad\", \"dial_code\": \"+235\", \"code\": \"TD\", \"flag\": \"🇹🇩\" }, { \"name\": \"Chile\", \"dial_code\": \"+56\", \"code\": \"CL\", \"flag\": \"🇨🇱\" }, { \"name\": \"China\", \"dial_code\": \"+86\", \"code\": \"CN\", \"flag\": \"🇨🇳\" }, { \"name\": \"Christmas Island\", \"dial_code\": \"+61\", \"code\": \"CX\", \"flag\": \"🇨🇽\" }, { \"name\": \"Cocos (Keeling) Islands\", \"dial_code\": \"+61\", \"code\": \"CC\", \"flag\": \"🇨🇨\" }, { \"name\": \"Colombia\", \"dial_code\": \"+57\", \"code\": \"CO\", \"flag\": \"🇨🇴\" }, { \"name\": \"Comoros\", \"dial_code\": \"+269\", \"code\": \"KM\", \"flag\": \"🇰🇲\" }, { \"name\": \"Congo\", \"dial_code\": \"+242\", \"code\": \"CG\", \"flag\": \"🇨🇬\" }, { \"name\": \"Congo, The Democratic Republic of the\", \"dial_code\": \"+243\", \"code\": \"CD\", \"flag\": \"🇨🇩\" }, { \"name\": \"Cook Islands\", \"dial_code\": \"+682\", \"code\": \"CK\", \"flag\": \"🇨🇰\" }, { \"name\": \"Costa Rica\", \"dial_code\": \"+506\", \"code\": \"CR\", \"flag\": \"🇨🇷\" }, { \"name\": \"Cote d'Ivoire\", \"dial_code\": \"+225\", \"code\": \"CI\", \"flag\": \"🇨🇮\" }, { \"name\": \"Croatia\", \"dial_code\": \"+385\", \"code\": \"HR\", \"flag\": \"🇭🇷\" }, { \"name\": \"Cuba\", \"dial_code\": \"+53\", \"code\": \"CU\", \"flag\": \"🇨🇺\" }, { \"name\": \"Cyprus\", \"dial_code\": \"+537\", \"code\": \"CY\", \"flag\": \"🇨🇾\" }, { \"name\": \"Czech Republic\", \"dial_code\": \"+420\", \"code\": \"CZ\", \"flag\": \"🇨🇿\" }, { \"name\": \"Denmark\", \"dial_code\": \"+45\", \"code\": \"DK\", \"flag\": \"🇩🇰\" }, { \"name\": \"Djibouti\", \"dial_code\": \"+253\", \"code\": \"DJ\", \"flag\": \"🇩🇯\" }, { \"name\": \"Dominica\", \"dial_code\": \"+1767\", \"code\": \"DM\", \"flag\": \"🇩🇲\" }, { \"name\": \"Dominican Republic\", \"dial_code\": \"+1\", \"code\": \"DO\", \"flag\": \"🇩🇴\" }, { \"name\": \"Ecuador\", \"dial_code\": \"+593\", \"code\": \"EC\", \"flag\": \"🇪🇨\" }, { \"name\": \"Egypt\", \"dial_code\": \"+20\", \"code\": \"EG\", \"flag\": \"🇪🇬\" }, { \"name\": \"El Salvador\", \"dial_code\": \"+503\", \"code\": \"SV\", \"flag\": \"🇸🇻\" }, { \"name\": \"Equatorial Guinea\", \"dial_code\": \"+240\", \"code\": \"GQ\", \"flag\": \"🇬🇶\" }, { \"name\": \"Eritrea\", \"dial_code\": \"+291\", \"code\": \"ER\", \"flag\": \"🇪🇷\" }, { \"name\": \"Estonia\", \"dial_code\": \"+372\", \"code\": \"EE\", \"flag\": \"🇪🇪\" }, { \"name\": \"Ethiopia\", \"dial_code\": \"+251\", \"code\": \"ET\", \"flag\": \"🇪🇹\" }, { \"name\": \"Falkland Islands (Malvinas)\", \"dial_code\": \"+500\", \"code\": \"FK\", \"flag\": \"🇫🇰\" }, { \"name\": \"Faroe Islands\", \"dial_code\": \"+298\", \"code\": \"FO\", \"flag\": \"🇫🇴\" }, { \"name\": \"Fiji\", \"dial_code\": \"+679\", \"code\": \"FJ\", \"flag\": \"🇫🇯\" }, { \"name\": \"Finland\", \"dial_code\": \"+358\", \"code\": \"FI\", \"flag\": \"🇫🇮\" }, { \"name\": \"France\", \"dial_code\": \"+33\", \"code\": \"FR\", \"flag\": \"🇫🇷\" }, { \"name\": \"French Guiana\", \"dial_code\": \"+594\", \"code\": \"GF\", \"flag\": \"🇬🇫\" }, { \"name\": \"French Polynesia\", \"dial_code\": \"+689\", \"code\": \"PF\", \"flag\": \"🇵🇫\" }, { \"name\": \"Gabon\", \"dial_code\": \"+241\", \"code\": \"GA\", \"flag\": \"🇬🇦\" }, { \"name\": \"Gambia\", \"dial_code\": \"+220\", \"code\": \"GM\", \"flag\": \"🇬🇲\" }, { \"name\": \"Georgia\", \"dial_code\": \"+995\", \"code\": \"GE\", \"flag\": \"🇬🇪\" }, { \"name\": \"Germany\", \"dial_code\": \"+49\", \"code\": \"DE\", \"flag\": \"🇩🇪\" }, { \"name\": \"Ghana\", \"dial_code\": \"+233\", \"code\": \"GH\", \"flag\": \"🇬🇭\" }, { \"name\": \"Gibraltar\", \"dial_code\": \"+350\", \"code\": \"GI\", \"flag\": \"🇬🇮\" }, { \"name\": \"Greece\", \"dial_code\": \"+30\", \"code\": \"GR\", \"flag\": \"🇬🇷\" }, { \"name\": \"Greenland\", \"dial_code\": \"+299\", \"code\": \"GL\", \"flag\": \"🇬🇱\" }, { \"name\": \"Grenada\", \"dial_code\": \"+1473\", \"code\": \"GD\", \"flag\": \"🇬🇩\" }, { \"name\": \"Guadeloupe\", \"dial_code\": \"+590\", \"code\": \"GP\", \"flag\": \"🇬🇵\" }, { \"name\": \"Guam\", \"dial_code\": \"+1671\", \"code\": \"GU\", \"flag\": \"🇬🇺\" }, { \"name\": \"Guatemala\", \"dial_code\": \"+502\", \"code\": \"GT\", \"flag\": \"🇬🇹\" }, { \"name\": \"Guernsey\", \"dial_code\": \"+44\", \"code\": \"GG\", \"flag\": \"🇬🇬\" }, { \"name\": \"Guinea\", \"dial_code\": \"+224\", \"code\": \"GN\", \"flag\": \"🇬🇳\" }, { \"name\": \"Guinea-Bissau\", \"dial_code\": \"+245\", \"code\": \"GW\", \"flag\": \"🇬🇼\" }, { \"name\": \"Guyana\", \"dial_code\": \"+595\", \"code\": \"GY\", \"flag\": \"🇬🇾\" }, { \"name\": \"Haiti\", \"dial_code\": \"+509\", \"code\": \"HT\", \"flag\": \"🇭🇹\" }, { \"name\": \"Holy See (Vatican City State)\", \"dial_code\": \"+379\", \"code\": \"VA\", \"flag\": \"🇻🇦\" }, { \"name\": \"Honduras\", \"dial_code\": \"+504\", \"code\": \"HN\", \"flag\": \"🇭🇳\" }, { \"name\": \"Hong Kong\", \"dial_code\": \"+852\", \"code\": \"HK\", \"flag\": \"🇭🇰\" }, { \"name\": \"Hungary\", \"dial_code\": \"+36\", \"code\": \"HU\", \"flag\": \"🇭🇺\" }, { \"name\": \"Iceland\", \"dial_code\": \"+354\", \"code\": \"IS\", \"flag\": \"🇮🇸\" }, { \"name\": \"India\", \"dial_code\": \"+91\", \"code\": \"IN\", \"preferred\": true, \"flag\": \"🇮🇳\" }, { \"name\": \"Indonesia\", \"dial_code\": \"+62\", \"code\": \"ID\", \"flag\": \"🇮🇩\" }, { \"name\": \"Iran, Islamic Republic of\", \"dial_code\": \"+98\", \"code\": \"IR\", \"flag\": \"🇮🇷\" }, { \"name\": \"Iraq\", \"dial_code\": \"+964\", \"code\": \"IQ\", \"flag\": \"🇮🇶\" }, { \"name\": \"Ireland\", \"dial_code\": \"+353\", \"code\": \"IE\", \"flag\": \"🇮🇪\" }, { \"name\": \"Isle of Man\", \"dial_code\": \"+44\", \"code\": \"IM\", \"flag\": \"🇮🇲\" }, { \"name\": \"Israel\", \"dial_code\": \"+972\", \"code\": \"IL\", \"flag\": \"🇮🇱\" }, { \"name\": \"Italy\", \"dial_code\": \"+39\", \"code\": \"IT\", \"flag\": \"🇮🇹\" }, { \"name\": \"Jamaica\", \"dial_code\": \"+1876\", \"code\": \"JM\", \"flag\": \"🇯🇲\" }, { \"name\": \"Japan\", \"dial_code\": \"+81\", \"code\": \"JP\", \"flag\": \"🇯🇵\" }, { \"name\": \"Jersey\", \"dial_code\": \"+44\", \"code\": \"JE\", \"flag\": \"🇯🇪\" }, { \"name\": \"Jordan\", \"dial_code\": \"+962\", \"code\": \"JO\", \"flag\": \"🇯🇴\" }, { \"name\": \"Kazakhstan\", \"dial_code\": \"+7\", \"code\": \"KZ\", \"flag\": \"🇰🇿\" }, { \"name\": \"Kenya\", \"dial_code\": \"+254\", \"code\": \"KE\", \"flag\": \"🇰🇪\" }, { \"name\": \"Kiribati\", \"dial_code\": \"+686\", \"code\": \"KI\", \"flag\": \"🇰🇮\" }, { \"name\": \"Korea, Democratic People's Republic of\", \"dial_code\": \"+850\", \"code\": \"KP\", \"flag\": \"🇰🇵\" }, { \"name\": \"Korea, Republic of\", \"dial_code\": \"+82\", \"code\": \"KR\", \"flag\": \"🇰🇷\" }, { \"name\": \"Kuwait\", \"dial_code\": \"+965\", \"code\": \"KW\", \"flag\": \"🇰🇼\" }, { \"name\": \"Kyrgyzstan\", \"dial_code\": \"+996\", \"code\": \"KG\", \"flag\": \"🇰🇬\" }, { \"name\": \"Lao People's Democratic Republic\", \"dial_code\": \"+856\", \"code\": \"LA\", \"flag\": \"🇱🇦\" }, { \"name\": \"Latvia\", \"dial_code\": \"+371\", \"code\": \"LV\", \"flag\": \"🇱🇻\" }, { \"name\": \"Lebanon\", \"dial_code\": \"+961\", \"code\": \"LB\", \"flag\": \"🇱🇧\" }, { \"name\": \"Lesotho\", \"dial_code\": \"+266\", \"code\": \"LS\", \"flag\": \"🇱🇸\" }, { \"name\": \"Liberia\", \"dial_code\": \"+231\", \"code\": \"LR\", \"flag\": \"🇱🇷\" }, { \"name\": \"Libyan Arab Jamahiriya\", \"dial_code\": \"+218\", \"code\": \"LY\", \"flag\": \"🇱🇾\" }, { \"name\": \"Liechtenstein\", \"dial_code\": \"+423\", \"code\": \"LI\", \"flag\": \"🇱🇮\" }, { \"name\": \"Lithuania\", \"dial_code\": \"+370\", \"code\": \"LT\", \"flag\": \"🇱🇹\" }, { \"name\": \"Luxembourg\", \"dial_code\": \"+352\", \"code\": \"LU\", \"flag\": \"🇱🇺\" }, { \"name\": \"Macao\", \"dial_code\": \"+853\", \"code\": \"MO\", \"flag\": \"🇲🇴\" }, { \"name\": \"Macedonia, The Former Yugoslav Republic of\", \"dial_code\": \"+389\", \"code\": \"MK\", \"flag\": \"🇲🇰\" }, { \"name\": \"Madagascar\", \"dial_code\": \"+261\", \"code\": \"MG\", \"flag\": \"🇲🇬\" }, { \"name\": \"Malawi\", \"dial_code\": \"+265\", \"code\": \"MW\", \"flag\": \"🇲🇼\" }, { \"name\": \"Malaysia\", \"dial_code\": \"+60\", \"code\": \"MY\", \"flag\": \"🇲🇾\" }, { \"name\": \"Maldives\", \"dial_code\": \"+960\", \"code\": \"MV\", \"flag\": \"🇲🇻\" }, { \"name\": \"Mali\", \"dial_code\": \"+223\", \"code\": \"ML\", \"flag\": \"🇲🇱\" }, { \"name\": \"Malta\", \"dial_code\": \"+356\", \"code\": \"MT\", \"flag\": \"🇲🇹\" }, { \"name\": \"Marshall Islands\", \"dial_code\": \"+692\", \"code\": \"MH\", \"flag\": \"🇲🇭\" }, { \"name\": \"Martinique\", \"dial_code\": \"+596\", \"code\": \"MQ\", \"flag\": \"🇲🇶\" }, { \"name\": \"Mauritania\", \"dial_code\": \"+222\", \"code\": \"MR\", \"flag\": \"🇲🇷\" }, { \"name\": \"Mauritius\", \"dial_code\": \"+230\", \"code\": \"MU\", \"flag\": \"🇲🇺\" }, { \"name\": \"Mayotte\", \"dial_code\": \"+262\", \"code\": \"YT\", \"flag\": \"🇾🇹\" }, { \"name\": \"Mexico\", \"dial_code\": \"+52\", \"code\": \"MX\", \"flag\": \"🇲🇽\" }, { \"name\": \"Micronesia, Federated States of\", \"dial_code\": \"+691\", \"code\": \"FM\", \"flag\": \"🇫🇲\" }, { \"name\": \"Moldova, Republic of\", \"dial_code\": \"+373\", \"code\": \"MD\", \"flag\": \"🇲🇩\" }, { \"name\": \"Monaco\", \"dial_code\": \"+377\", \"code\": \"MC\", \"flag\": \"🇲🇨\" }, { \"name\": \"Mongolia\", \"dial_code\": \"+976\", \"code\": \"MN\", \"flag\": \"🇲🇳\" }, { \"name\": \"Montenegro\", \"dial_code\": \"+382\", \"code\": \"ME\", \"flag\": \"🇲🇪\" }, { \"name\": \"Montserrat\", \"dial_code\": \"+1664\", \"code\": \"MS\", \"flag\": \"🇲🇸\" }, { \"name\": \"Morocco\", \"dial_code\": \"+212\", \"code\": \"MA\", \"flag\": \"🇲🇦\" }, { \"name\": \"Mozambique\", \"dial_code\": \"+258\", \"code\": \"MZ\", \"flag\": \"🇲🇿\" }, { \"name\": \"Myanmar\", \"dial_code\": \"+95\", \"code\": \"MM\", \"flag\": \"🇲🇲\" }, { \"name\": \"Namibia\", \"dial_code\": \"+264\", \"code\": \"NA\", \"flag\": \"🇳🇦\" }, { \"name\": \"Nauru\", \"dial_code\": \"+674\", \"code\": \"NR\", \"flag\": \"🇳🇷\" }, { \"name\": \"Nepal\", \"dial_code\": \"+977\", \"code\": \"NP\", \"flag\": \"🇳🇵\" }, { \"name\": \"Netherlands\", \"dial_code\": \"+31\", \"code\": \"NL\", \"flag\": \"🇳🇱\" }, { \"name\": \"Netherlands Antilles\", \"dial_code\": \"+599\", \"code\": \"AN\", \"flag\": \"🇦🇳\" }, { \"name\": \"New Caledonia\", \"dial_code\": \"+687\", \"code\": \"NC\", \"flag\": \"🇳🇨\" }, { \"name\": \"New Zealand\", \"dial_code\": \"+64\", \"code\": \"NZ\", \"flag\": \"🇳🇿\" }, { \"name\": \"Nicaragua\", \"dial_code\": \"+505\", \"code\": \"NI\", \"flag\": \"🇳🇮\" }, { \"name\": \"Niger\", \"dial_code\": \"+227\", \"code\": \"NE\", \"flag\": \"🇳🇪\" }, { \"name\": \"Nigeria\", \"dial_code\": \"+234\", \"code\": \"NG\", \"flag\": \"🇳🇬\" }, { \"name\": \"Niue\", \"dial_code\": \"+683\", \"code\": \"NU\", \"flag\": \"🇳🇺\" }, { \"name\": \"Norfolk Island\", \"dial_code\": \"+672\", \"code\": \"NF\", \"flag\": \"🇳🇫\" }, { \"name\": \"Northern Mariana Islands\", \"dial_code\": \"+1670\", \"code\": \"MP\", \"flag\": \"🇲🇵\" }, { \"name\": \"Norway\", \"dial_code\": \"+47\", \"code\": \"NO\", \"flag\": \"🇳🇴\" }, { \"name\": \"Oman\", \"dial_code\": \"+968\", \"code\": \"OM\", \"flag\": \"🇴🇲\" }, { \"name\": \"Pakistan\", \"dial_code\": \"+92\", \"code\": \"PK\", \"flag\": \"🇵🇰\" }, { \"name\": \"Palau\", \"dial_code\": \"+680\", \"code\": \"PW\", \"flag\": \"🇵🇼\" }, { \"name\": \"Palestinian Territory, Occupied\", \"dial_code\": \"+970\", \"code\": \"PS\", \"flag\": \"🇵🇸\" }, { \"name\": \"Panama\", \"dial_code\": \"+507\", \"code\": \"PA\", \"flag\": \"🇵🇦\" }, { \"name\": \"Papua New Guinea\", \"dial_code\": \"+675\", \"code\": \"PG\", \"flag\": \"🇵🇬\" }, { \"name\": \"Paraguay\", \"dial_code\": \"+595\", \"code\": \"PY\", \"flag\": \"🇵🇾\" }, { \"name\": \"Peru\", \"dial_code\": \"+51\", \"code\": \"PE\", \"flag\": \"🇵🇪\" }, { \"name\": \"Philippines\", \"dial_code\": \"+63\", \"code\": \"PH\", \"flag\": \"🇵🇭\" }, { \"name\": \"Pitcairn\", \"dial_code\": \"+872\", \"code\": \"PN\", \"flag\": \"🇵🇳\" }, { \"name\": \"Poland\", \"dial_code\": \"+48\", \"code\": \"PL\", \"flag\": \"🇵🇱\" }, { \"name\": \"Portugal\", \"dial_code\": \"+351\", \"code\": \"PT\", \"flag\": \"🇵🇹\" }, { \"name\": \"Puerto Rico\", \"dial_code\": \"+1939\", \"code\": \"PR\", \"flag\": \"🇵🇷\" }, { \"name\": \"Qatar\", \"dial_code\": \"+974\", \"code\": \"QA\", \"flag\": \"🇶🇦\" }, { \"name\": \"Romania\", \"dial_code\": \"+40\", \"code\": \"RO\", \"flag\": \"🇷🇴\" }, { \"name\": \"Russia\", \"dial_code\": \"+7\", \"code\": \"RU\", \"flag\": \"🇷🇺\" }, { \"name\": \"Rwanda\", \"dial_code\": \"+250\", \"code\": \"RW\", \"flag\": \"🇷🇼\" }, { \"name\": \"Réunion\", \"dial_code\": \"+262\", \"code\": \"RE\", \"flag\": \"🇷🇪\" }, { \"name\": \"Saint Barthélemy\", \"dial_code\": \"+590\", \"code\": \"BL\", \"flag\": \"🇧🇱\" }, { \"name\": \"Saint Helena, Ascension and Tristan Da Cunha\", \"dial_code\": \"+290\", \"code\": \"SH\", \"flag\": \"🇸🇭\" }, { \"name\": \"Saint Kitts and Nevis\", \"dial_code\": \"+1869\", \"code\": \"KN\", \"flag\": \"🇰🇳\" }, { \"name\": \"Saint Lucia\", \"dial_code\": \"+1758\", \"code\": \"LC\", \"flag\": \"🇱🇨\" }, { \"name\": \"Saint Martin\", \"dial_code\": \"+590\", \"code\": \"MF\", \"flag\": \"🇲🇫\" }, { \"name\": \"Saint Pierre and Miquelon\", \"dial_code\": \"+508\", \"code\": \"PM\", \"flag\": \"🇵🇲\" }, { \"name\": \"Saint Vincent and the Grenadines\", \"dial_code\": \"+1784\", \"code\": \"VC\", \"flag\": \"🇻🇨\" }, { \"name\": \"Samoa\", \"dial_code\": \"+685\", \"code\": \"WS\", \"flag\": \"🇼🇸\" }, { \"name\": \"San Marino\", \"dial_code\": \"+378\", \"code\": \"SM\", \"flag\": \"🇸🇲\" }, { \"name\": \"Sao Tome and Principe\", \"dial_code\": \"+239\", \"code\": \"ST\", \"flag\": \"🇸🇹\" }, { \"name\": \"Saudi Arabia\", \"dial_code\": \"+966\", \"code\": \"SA\", \"flag\": \"🇸🇦\" }, { \"name\": \"Senegal\", \"dial_code\": \"+221\", \"code\": \"SN\", \"flag\": \"🇸🇳\" }, { \"name\": \"Serbia\", \"dial_code\": \"+381\", \"code\": \"RS\", \"flag\": \"🇷🇸\" }, { \"name\": \"Seychelles\", \"dial_code\": \"+248\", \"code\": \"SC\", \"flag\": \"🇸🇨\" }, { \"name\": \"Sierra Leone\", \"dial_code\": \"+232\", \"code\": \"SL\", \"flag\": \"🇸🇱\" }, { \"name\": \"Singapore\", \"dial_code\": \"+65\", \"code\": \"SG\", \"flag\": \"🇸🇬\" }, { \"name\": \"Slovakia\", \"dial_code\": \"+421\", \"code\": \"SK\", \"flag\": \"🇸🇰\" }, { \"name\": \"Slovenia\", \"dial_code\": \"+386\", \"code\": \"SI\", \"flag\": \"🇸🇮\" }, { \"name\": \"Solomon Islands\", \"dial_code\": \"+677\", \"code\": \"SB\", \"flag\": \"🇸🇧\" }, { \"name\": \"Somalia\", \"dial_code\": \"+252\", \"code\": \"SO\", \"flag\": \"🇸🇴\" }, { \"name\": \"South Africa\", \"dial_code\": \"+27\", \"code\": \"ZA\", \"flag\": \"🇿🇦\" }, { \"name\": \"South Georgia and the South Sandwich Islands\", \"dial_code\": \"+500\", \"code\": \"GS\", \"flag\": \"🇬🇸\" }, { \"name\": \"Spain\", \"dial_code\": \"+34\", \"code\": \"ES\", \"flag\": \"🇪🇸\" }, { \"name\": \"Sri Lanka\", \"dial_code\": \"+94\", \"code\": \"LK\", \"flag\": \"🇱🇰\" }, { \"name\": \"Sudan\", \"dial_code\": \"+249\", \"code\": \"SD\", \"flag\": \"🇸🇩\" }, { \"name\": \"Suriname\", \"dial_code\": \"+597\", \"code\": \"SR\", \"flag\": \"🇸🇷\" }, { \"name\": \"Svalbard and Jan Mayen\", \"dial_code\": \"+47\", \"code\": \"SJ\", \"flag\": \"🇸🇯\" }, { \"name\": \"Swaziland\", \"dial_code\": \"+268\", \"code\": \"SZ\", \"flag\": \"🇸🇿\" }, { \"name\": \"Sweden\", \"dial_code\": \"+46\", \"code\": \"SE\", \"flag\": \"🇸🇪\" }, { \"name\": \"Switzerland\", \"dial_code\": \"+41\", \"code\": \"CH\", \"flag\": \"🇨🇭\" }, { \"name\": \"Syrian Arab Republic\", \"dial_code\": \"+963\", \"code\": \"SY\", \"flag\": \"🇸🇾\" }, { \"name\": \"Taiwan, Province of China\", \"dial_code\": \"+886\", \"code\": \"TW\", \"flag\": \"🇹🇼\" }, { \"name\": \"Tajikistan\", \"dial_code\": \"+992\", \"code\": \"TJ\", \"flag\": \"🇹🇯\" }, { \"name\": \"Tanzania, United Republic of\", \"dial_code\": \"+255\", \"code\": \"TZ\", \"flag\": \"🇹🇿\" }, { \"name\": \"Thailand\", \"dial_code\": \"+66\", \"code\": \"TH\", \"flag\": \"🇹🇭\" }, { \"name\": \"Timor-Leste\", \"dial_code\": \"+670\", \"code\": \"TL\", \"flag\": \"🇹🇱\" }, { \"name\": \"Togo\", \"dial_code\": \"+228\", \"code\": \"TG\", \"flag\": \"🇹🇬\" }, { \"name\": \"Tokelau\", \"dial_code\": \"+690\", \"code\": \"TK\", \"flag\": \"🇹🇰\" }, { \"name\": \"Tonga\", \"dial_code\": \"+676\", \"code\": \"TO\", \"flag\": \"🇹🇴\" }, { \"name\": \"Trinidad and Tobago\", \"dial_code\": \"+1868\", \"code\": \"TT\", \"flag\": \"🇹🇹\" }, { \"name\": \"Tunisia\", \"dial_code\": \"+216\", \"code\": \"TN\", \"flag\": \"🇹🇳\" }, { \"name\": \"Turkey\", \"dial_code\": \"+90\", \"code\": \"TR\", \"flag\": \"🇹🇷\" }, { \"name\": \"Turkmenistan\", \"dial_code\": \"+993\", \"code\": \"TM\", \"flag\": \"🇹🇲\" }, { \"name\": \"Turks and Caicos Islands\", \"dial_code\": \"+1649\", \"code\": \"TC\", \"flag\": \"🇹🇨\" }, { \"name\": \"Tuvalu\", \"dial_code\": \"+688\", \"code\": \"TV\", \"flag\": \"🇹🇻\" }, { \"name\": \"Uganda\", \"dial_code\": \"+256\", \"code\": \"UG\", \"flag\": \"🇺🇬\" }, { \"name\": \"Ukraine\", \"dial_code\": \"+380\", \"code\": \"UA\", \"flag\": \"🇺🇦\" }, { \"name\": \"United Arab Emirates\", \"dial_code\": \"+971\", \"code\": \"AE\", \"preferred\": true, \"flag\": \"🇦🇪\" }, { \"name\": \"United Kingdom\", \"dial_code\": \"+44\", \"code\": \"GB\", \"preferred\": true, \"flag\": \"🇬🇧\" }, { \"name\": \"United States\", \"dial_code\": \"+1\", \"code\": \"US\", \"preferred\": true, \"flag\": \"🇺🇸\" }, { \"name\": \"Uruguay\", \"dial_code\": \"+598\", \"code\": \"UY\", \"flag\": \"🇺🇾\" }, { \"name\": \"Uzbekistan\", \"dial_code\": \"+998\", \"code\": \"UZ\", \"flag\": \"🇺🇿\" }, { \"name\": \"Vanuatu\", \"dial_code\": \"+678\", \"code\": \"VU\", \"flag\": \"🇻🇺\" }, { \"name\": \"Venezuela, Bolivarian Republic of\", \"dial_code\": \"+58\", \"code\": \"VE\", \"flag\": \"🇻🇪\" }, { \"name\": \"Viet Nam\", \"dial_code\": \"+84\", \"code\": \"VN\", \"flag\": \"🇻🇳\" }, { \"name\": \"Virgin Islands, British\", \"dial_code\": \"+1284\", \"code\": \"VG\", \"flag\": \"🇻🇬\" }, { \"name\": \"Virgin Islands, U.S.\", \"dial_code\": \"+1340\", \"code\": \"VI\", \"flag\": \"🇻🇮\" }, { \"name\": \"Wallis and Futuna\", \"dial_code\": \"+681\", \"code\": \"WF\", \"flag\": \"🇼🇫\" }, { \"name\": \"Yemen\", \"dial_code\": \"+967\", \"code\": \"YE\", \"flag\": \"🇾🇪\" }, { \"name\": \"Zambia\", \"dial_code\": \"+260\", \"code\": \"ZM\", \"flag\": \"🇿🇲\" }, { \"name\": \"Zimbabwe\", \"dial_code\": \"+263\", \"code\": \"ZW\", \"flag\": \"🇿🇼\" }, { \"name\": \"Åland Islands\", \"dial_code\": \"+358\", \"code\": \"AX\", \"flag\": \"🇦🇽\" } ]" + private const val countriesJson = "[ { \"name\": \"Afghanistan\", \"dial_code\": \"+93\", \"code\": \"AF\", \"flag\": \"🇦🇫\" }, { \"name\": \"Albania\", \"dial_code\": \"+355\", \"code\": \"AL\", \"flag\": \"🇦🇱\" }, { \"name\": \"Algeria\", \"dial_code\": \"+213\", \"code\": \"DZ\", \"flag\": \"🇩🇿\" }, { \"name\": \"AmericanSamoa\", \"dial_code\": \"+1684\", \"code\": \"AS\", \"flag\": \"🇦🇸\" }, { \"name\": \"Andorra\", \"dial_code\": \"+376\", \"code\": \"AD\", \"flag\": \"🇦🇩\" }, { \"name\": \"Angola\", \"dial_code\": \"+244\", \"code\": \"AO\", \"flag\": \"🇦🇴\" }, { \"name\": \"Anguilla\", \"dial_code\": \"+1264\", \"code\": \"AI\", \"flag\": \"🇦🇮\" }, { \"name\": \"Antarctica\", \"dial_code\": \"+672\", \"code\": \"AQ\", \"flag\": \"🇦🇶\" }, { \"name\": \"Antigua and Barbuda\", \"dial_code\": \"+1268\", \"code\": \"AG\", \"flag\": \"🇦🇬\" }, { \"name\": \"Argentina\", \"dial_code\": \"+54\", \"code\": \"AR\", \"flag\": \"🇦🇷\" }, { \"name\": \"Armenia\", \"dial_code\": \"+374\", \"code\": \"AM\", \"flag\": \"🇦🇲\" }, { \"name\": \"Aruba\", \"dial_code\": \"+297\", \"code\": \"AW\", \"flag\": \"🇦🇼\" }, { \"name\": \"Australia\", \"dial_code\": \"+61\", \"code\": \"AU\", \"preferred\": true, \"flag\": \"🇦🇺\" }, { \"name\": \"Austria\", \"dial_code\": \"+43\", \"code\": \"AT\", \"flag\": \"🇦🇹\" }, { \"name\": \"Azerbaijan\", \"dial_code\": \"+994\", \"code\": \"AZ\", \"flag\": \"🇦🇿\" }, { \"name\": \"Bahamas\", \"dial_code\": \"+1242\", \"code\": \"BS\", \"flag\": \"🇧🇸\" }, { \"name\": \"Bahrain\", \"dial_code\": \"+973\", \"code\": \"BH\", \"flag\": \"🇧🇭\" }, { \"name\": \"Bangladesh\", \"dial_code\": \"+880\", \"code\": \"BD\", \"flag\": \"🇧🇩\" }, { \"name\": \"Barbados\", \"dial_code\": \"+1246\", \"code\": \"BB\", \"flag\": \"🇧🇧\" }, { \"name\": \"Belarus\", \"dial_code\": \"+375\", \"code\": \"BY\", \"flag\": \"🇧🇾\" }, { \"name\": \"Belgium\", \"dial_code\": \"+32\", \"code\": \"BE\", \"flag\": \"🇧🇪\" }, { \"name\": \"Belize\", \"dial_code\": \"+501\", \"code\": \"BZ\", \"flag\": \"🇧🇿\" }, { \"name\": \"Benin\", \"dial_code\": \"+229\", \"code\": \"BJ\", \"flag\": \"🇧🇯\" }, { \"name\": \"Bermuda\", \"dial_code\": \"+1441\", \"code\": \"BM\", \"flag\": \"🇧🇲\" }, { \"name\": \"Bhutan\", \"dial_code\": \"+975\", \"code\": \"BT\", \"flag\": \"🇧🇹\" }, { \"name\": \"Bolivia, Plurinational State of\", \"dial_code\": \"+591\", \"code\": \"BO\", \"flag\": \"🇧🇴\" }, { \"name\": \"Bosnia and Herzegovina\", \"dial_code\": \"+387\", \"code\": \"BA\", \"flag\": \"🇧🇦\" }, { \"name\": \"Botswana\", \"dial_code\": \"+267\", \"code\": \"BW\", \"flag\": \"🇧🇼\" }, { \"name\": \"Brazil\", \"dial_code\": \"+55\", \"code\": \"BR\", \"flag\": \"🇧🇷\" }, { \"name\": \"British Indian Ocean Territory\", \"dial_code\": \"+246\", \"code\": \"IO\", \"flag\": \"🇮🇴\" }, { \"name\": \"Brunei Darussalam\", \"dial_code\": \"+673\", \"code\": \"BN\", \"flag\": \"🇧🇳\" }, { \"name\": \"Bulgaria\", \"dial_code\": \"+359\", \"code\": \"BG\", \"flag\": \"🇧🇬\" }, { \"name\": \"Burkina Faso\", \"dial_code\": \"+226\", \"code\": \"BF\", \"flag\": \"🇧🇫\" }, { \"name\": \"Burundi\", \"dial_code\": \"+257\", \"code\": \"BI\", \"flag\": \"🇧🇮\" }, { \"name\": \"Cambodia\", \"dial_code\": \"+855\", \"code\": \"KH\", \"flag\": \"🇰🇭\" }, { \"name\": \"Cameroon\", \"dial_code\": \"+237\", \"code\": \"CM\", \"flag\": \"🇨🇲\" }, { \"name\": \"Canada\", \"dial_code\": \"+1\", \"code\": \"CA\", \"flag\": \"🇨🇦\" }, { \"name\": \"Cape Verde\", \"dial_code\": \"+238\", \"code\": \"CV\", \"flag\": \"🇨🇻\" }, { \"name\": \"Cayman Islands\", \"dial_code\": \"+345\", \"code\": \"KY\", \"flag\": \"🇰🇾\" }, { \"name\": \"Central African Republic\", \"dial_code\": \"+236\", \"code\": \"CF\", \"flag\": \"🇨🇫\" }, { \"name\": \"Chad\", \"dial_code\": \"+235\", \"code\": \"TD\", \"flag\": \"🇹🇩\" }, { \"name\": \"Chile\", \"dial_code\": \"+56\", \"code\": \"CL\", \"flag\": \"🇨🇱\" }, { \"name\": \"China\", \"dial_code\": \"+86\", \"code\": \"CN\", \"flag\": \"🇨🇳\" }, { \"name\": \"Christmas Island\", \"dial_code\": \"+61\", \"code\": \"CX\", \"flag\": \"🇨🇽\" }, { \"name\": \"Cocos (Keeling) Islands\", \"dial_code\": \"+61\", \"code\": \"CC\", \"flag\": \"🇨🇨\" }, { \"name\": \"Colombia\", \"dial_code\": \"+57\", \"code\": \"CO\", \"flag\": \"🇨🇴\" }, { \"name\": \"Comoros\", \"dial_code\": \"+269\", \"code\": \"KM\", \"flag\": \"🇰🇲\" }, { \"name\": \"Congo\", \"dial_code\": \"+242\", \"code\": \"CG\", \"flag\": \"🇨🇬\" }, { \"name\": \"Congo, The Democratic Republic of the\", \"dial_code\": \"+243\", \"code\": \"CD\", \"flag\": \"🇨🇩\" }, { \"name\": \"Cook Islands\", \"dial_code\": \"+682\", \"code\": \"CK\", \"flag\": \"🇨🇰\" }, { \"name\": \"Costa Rica\", \"dial_code\": \"+506\", \"code\": \"CR\", \"flag\": \"🇨🇷\" }, { \"name\": \"Cote d'Ivoire\", \"dial_code\": \"+225\", \"code\": \"CI\", \"flag\": \"🇨🇮\" }, { \"name\": \"Croatia\", \"dial_code\": \"+385\", \"code\": \"HR\", \"flag\": \"🇭🇷\" }, { \"name\": \"Cuba\", \"dial_code\": \"+53\", \"code\": \"CU\", \"flag\": \"🇨🇺\" }, { \"name\": \"Cyprus\", \"dial_code\": \"+357\", \"code\": \"CY\", \"flag\": \"🇨🇾\" }, { \"name\": \"Czech Republic\", \"dial_code\": \"+420\", \"code\": \"CZ\", \"flag\": \"🇨🇿\" }, { \"name\": \"Denmark\", \"dial_code\": \"+45\", \"code\": \"DK\", \"flag\": \"🇩🇰\" }, { \"name\": \"Djibouti\", \"dial_code\": \"+253\", \"code\": \"DJ\", \"flag\": \"🇩🇯\" }, { \"name\": \"Dominica\", \"dial_code\": \"+1767\", \"code\": \"DM\", \"flag\": \"🇩🇲\" }, { \"name\": \"Dominican Republic\", \"dial_code\": \"+1\", \"code\": \"DO\", \"flag\": \"🇩🇴\" }, { \"name\": \"Ecuador\", \"dial_code\": \"+593\", \"code\": \"EC\", \"flag\": \"🇪🇨\" }, { \"name\": \"Egypt\", \"dial_code\": \"+20\", \"code\": \"EG\", \"flag\": \"🇪🇬\" }, { \"name\": \"El Salvador\", \"dial_code\": \"+503\", \"code\": \"SV\", \"flag\": \"🇸🇻\" }, { \"name\": \"Equatorial Guinea\", \"dial_code\": \"+240\", \"code\": \"GQ\", \"flag\": \"🇬🇶\" }, { \"name\": \"Eritrea\", \"dial_code\": \"+291\", \"code\": \"ER\", \"flag\": \"🇪🇷\" }, { \"name\": \"Estonia\", \"dial_code\": \"+372\", \"code\": \"EE\", \"flag\": \"🇪🇪\" }, { \"name\": \"Ethiopia\", \"dial_code\": \"+251\", \"code\": \"ET\", \"flag\": \"🇪🇹\" }, { \"name\": \"Falkland Islands (Malvinas)\", \"dial_code\": \"+500\", \"code\": \"FK\", \"flag\": \"🇫🇰\" }, { \"name\": \"Faroe Islands\", \"dial_code\": \"+298\", \"code\": \"FO\", \"flag\": \"🇫🇴\" }, { \"name\": \"Fiji\", \"dial_code\": \"+679\", \"code\": \"FJ\", \"flag\": \"🇫🇯\" }, { \"name\": \"Finland\", \"dial_code\": \"+358\", \"code\": \"FI\", \"flag\": \"🇫🇮\" }, { \"name\": \"France\", \"dial_code\": \"+33\", \"code\": \"FR\", \"flag\": \"🇫🇷\" }, { \"name\": \"French Guiana\", \"dial_code\": \"+594\", \"code\": \"GF\", \"flag\": \"🇬🇫\" }, { \"name\": \"French Polynesia\", \"dial_code\": \"+689\", \"code\": \"PF\", \"flag\": \"🇵🇫\" }, { \"name\": \"Gabon\", \"dial_code\": \"+241\", \"code\": \"GA\", \"flag\": \"🇬🇦\" }, { \"name\": \"Gambia\", \"dial_code\": \"+220\", \"code\": \"GM\", \"flag\": \"🇬🇲\" }, { \"name\": \"Georgia\", \"dial_code\": \"+995\", \"code\": \"GE\", \"flag\": \"🇬🇪\" }, { \"name\": \"Germany\", \"dial_code\": \"+49\", \"code\": \"DE\", \"flag\": \"🇩🇪\" }, { \"name\": \"Ghana\", \"dial_code\": \"+233\", \"code\": \"GH\", \"flag\": \"🇬🇭\" }, { \"name\": \"Gibraltar\", \"dial_code\": \"+350\", \"code\": \"GI\", \"flag\": \"🇬🇮\" }, { \"name\": \"Greece\", \"dial_code\": \"+30\", \"code\": \"GR\", \"flag\": \"🇬🇷\" }, { \"name\": \"Greenland\", \"dial_code\": \"+299\", \"code\": \"GL\", \"flag\": \"🇬🇱\" }, { \"name\": \"Grenada\", \"dial_code\": \"+1473\", \"code\": \"GD\", \"flag\": \"🇬🇩\" }, { \"name\": \"Guadeloupe\", \"dial_code\": \"+590\", \"code\": \"GP\", \"flag\": \"🇬🇵\" }, { \"name\": \"Guam\", \"dial_code\": \"+1671\", \"code\": \"GU\", \"flag\": \"🇬🇺\" }, { \"name\": \"Guatemala\", \"dial_code\": \"+502\", \"code\": \"GT\", \"flag\": \"🇬🇹\" }, { \"name\": \"Guernsey\", \"dial_code\": \"+44\", \"code\": \"GG\", \"flag\": \"🇬🇬\" }, { \"name\": \"Guinea\", \"dial_code\": \"+224\", \"code\": \"GN\", \"flag\": \"🇬🇳\" }, { \"name\": \"Guinea-Bissau\", \"dial_code\": \"+245\", \"code\": \"GW\", \"flag\": \"🇬🇼\" }, { \"name\": \"Guyana\", \"dial_code\": \"+595\", \"code\": \"GY\", \"flag\": \"🇬🇾\" }, { \"name\": \"Haiti\", \"dial_code\": \"+509\", \"code\": \"HT\", \"flag\": \"🇭🇹\" }, { \"name\": \"Holy See (Vatican City State)\", \"dial_code\": \"+379\", \"code\": \"VA\", \"flag\": \"🇻🇦\" }, { \"name\": \"Honduras\", \"dial_code\": \"+504\", \"code\": \"HN\", \"flag\": \"🇭🇳\" }, { \"name\": \"Hong Kong\", \"dial_code\": \"+852\", \"code\": \"HK\", \"flag\": \"🇭🇰\" }, { \"name\": \"Hungary\", \"dial_code\": \"+36\", \"code\": \"HU\", \"flag\": \"🇭🇺\" }, { \"name\": \"Iceland\", \"dial_code\": \"+354\", \"code\": \"IS\", \"flag\": \"🇮🇸\" }, { \"name\": \"India\", \"dial_code\": \"+91\", \"code\": \"IN\", \"preferred\": true, \"flag\": \"🇮🇳\" }, { \"name\": \"Indonesia\", \"dial_code\": \"+62\", \"code\": \"ID\", \"flag\": \"🇮🇩\" }, { \"name\": \"Iran, Islamic Republic of\", \"dial_code\": \"+98\", \"code\": \"IR\", \"flag\": \"🇮🇷\" }, { \"name\": \"Iraq\", \"dial_code\": \"+964\", \"code\": \"IQ\", \"flag\": \"🇮🇶\" }, { \"name\": \"Ireland\", \"dial_code\": \"+353\", \"code\": \"IE\", \"flag\": \"🇮🇪\" }, { \"name\": \"Isle of Man\", \"dial_code\": \"+44\", \"code\": \"IM\", \"flag\": \"🇮🇲\" }, { \"name\": \"Israel\", \"dial_code\": \"+972\", \"code\": \"IL\", \"flag\": \"🇮🇱\" }, { \"name\": \"Italy\", \"dial_code\": \"+39\", \"code\": \"IT\", \"flag\": \"🇮🇹\" }, { \"name\": \"Jamaica\", \"dial_code\": \"+1876\", \"code\": \"JM\", \"flag\": \"🇯🇲\" }, { \"name\": \"Japan\", \"dial_code\": \"+81\", \"code\": \"JP\", \"flag\": \"🇯🇵\" }, { \"name\": \"Jersey\", \"dial_code\": \"+44\", \"code\": \"JE\", \"flag\": \"🇯🇪\" }, { \"name\": \"Jordan\", \"dial_code\": \"+962\", \"code\": \"JO\", \"flag\": \"🇯🇴\" }, { \"name\": \"Kazakhstan\", \"dial_code\": \"+7\", \"code\": \"KZ\", \"flag\": \"🇰🇿\" }, { \"name\": \"Kenya\", \"dial_code\": \"+254\", \"code\": \"KE\", \"flag\": \"🇰🇪\" }, { \"name\": \"Kiribati\", \"dial_code\": \"+686\", \"code\": \"KI\", \"flag\": \"🇰🇮\" }, { \"name\": \"Korea, Democratic People's Republic of\", \"dial_code\": \"+850\", \"code\": \"KP\", \"flag\": \"🇰🇵\" }, { \"name\": \"Korea, Republic of\", \"dial_code\": \"+82\", \"code\": \"KR\", \"flag\": \"🇰🇷\" }, { \"name\": \"Kuwait\", \"dial_code\": \"+965\", \"code\": \"KW\", \"flag\": \"🇰🇼\" }, { \"name\": \"Kyrgyzstan\", \"dial_code\": \"+996\", \"code\": \"KG\", \"flag\": \"🇰🇬\" }, { \"name\": \"Lao People's Democratic Republic\", \"dial_code\": \"+856\", \"code\": \"LA\", \"flag\": \"🇱🇦\" }, { \"name\": \"Latvia\", \"dial_code\": \"+371\", \"code\": \"LV\", \"flag\": \"🇱🇻\" }, { \"name\": \"Lebanon\", \"dial_code\": \"+961\", \"code\": \"LB\", \"flag\": \"🇱🇧\" }, { \"name\": \"Lesotho\", \"dial_code\": \"+266\", \"code\": \"LS\", \"flag\": \"🇱🇸\" }, { \"name\": \"Liberia\", \"dial_code\": \"+231\", \"code\": \"LR\", \"flag\": \"🇱🇷\" }, { \"name\": \"Libyan Arab Jamahiriya\", \"dial_code\": \"+218\", \"code\": \"LY\", \"flag\": \"🇱🇾\" }, { \"name\": \"Liechtenstein\", \"dial_code\": \"+423\", \"code\": \"LI\", \"flag\": \"🇱🇮\" }, { \"name\": \"Lithuania\", \"dial_code\": \"+370\", \"code\": \"LT\", \"flag\": \"🇱🇹\" }, { \"name\": \"Luxembourg\", \"dial_code\": \"+352\", \"code\": \"LU\", \"flag\": \"🇱🇺\" }, { \"name\": \"Macao\", \"dial_code\": \"+853\", \"code\": \"MO\", \"flag\": \"🇲🇴\" }, { \"name\": \"Macedonia, The Former Yugoslav Republic of\", \"dial_code\": \"+389\", \"code\": \"MK\", \"flag\": \"🇲🇰\" }, { \"name\": \"Madagascar\", \"dial_code\": \"+261\", \"code\": \"MG\", \"flag\": \"🇲🇬\" }, { \"name\": \"Malawi\", \"dial_code\": \"+265\", \"code\": \"MW\", \"flag\": \"🇲🇼\" }, { \"name\": \"Malaysia\", \"dial_code\": \"+60\", \"code\": \"MY\", \"flag\": \"🇲🇾\" }, { \"name\": \"Maldives\", \"dial_code\": \"+960\", \"code\": \"MV\", \"flag\": \"🇲🇻\" }, { \"name\": \"Mali\", \"dial_code\": \"+223\", \"code\": \"ML\", \"flag\": \"🇲🇱\" }, { \"name\": \"Malta\", \"dial_code\": \"+356\", \"code\": \"MT\", \"flag\": \"🇲🇹\" }, { \"name\": \"Marshall Islands\", \"dial_code\": \"+692\", \"code\": \"MH\", \"flag\": \"🇲🇭\" }, { \"name\": \"Martinique\", \"dial_code\": \"+596\", \"code\": \"MQ\", \"flag\": \"🇲🇶\" }, { \"name\": \"Mauritania\", \"dial_code\": \"+222\", \"code\": \"MR\", \"flag\": \"🇲🇷\" }, { \"name\": \"Mauritius\", \"dial_code\": \"+230\", \"code\": \"MU\", \"flag\": \"🇲🇺\" }, { \"name\": \"Mayotte\", \"dial_code\": \"+262\", \"code\": \"YT\", \"flag\": \"🇾🇹\" }, { \"name\": \"Mexico\", \"dial_code\": \"+52\", \"code\": \"MX\", \"flag\": \"🇲🇽\" }, { \"name\": \"Micronesia, Federated States of\", \"dial_code\": \"+691\", \"code\": \"FM\", \"flag\": \"🇫🇲\" }, { \"name\": \"Moldova, Republic of\", \"dial_code\": \"+373\", \"code\": \"MD\", \"flag\": \"🇲🇩\" }, { \"name\": \"Monaco\", \"dial_code\": \"+377\", \"code\": \"MC\", \"flag\": \"🇲🇨\" }, { \"name\": \"Mongolia\", \"dial_code\": \"+976\", \"code\": \"MN\", \"flag\": \"🇲🇳\" }, { \"name\": \"Montenegro\", \"dial_code\": \"+382\", \"code\": \"ME\", \"flag\": \"🇲🇪\" }, { \"name\": \"Montserrat\", \"dial_code\": \"+1664\", \"code\": \"MS\", \"flag\": \"🇲🇸\" }, { \"name\": \"Morocco\", \"dial_code\": \"+212\", \"code\": \"MA\", \"flag\": \"🇲🇦\" }, { \"name\": \"Mozambique\", \"dial_code\": \"+258\", \"code\": \"MZ\", \"flag\": \"🇲🇿\" }, { \"name\": \"Myanmar\", \"dial_code\": \"+95\", \"code\": \"MM\", \"flag\": \"🇲🇲\" }, { \"name\": \"Namibia\", \"dial_code\": \"+264\", \"code\": \"NA\", \"flag\": \"🇳🇦\" }, { \"name\": \"Nauru\", \"dial_code\": \"+674\", \"code\": \"NR\", \"flag\": \"🇳🇷\" }, { \"name\": \"Nepal\", \"dial_code\": \"+977\", \"code\": \"NP\", \"flag\": \"🇳🇵\" }, { \"name\": \"Netherlands\", \"dial_code\": \"+31\", \"code\": \"NL\", \"flag\": \"🇳🇱\" }, { \"name\": \"Netherlands Antilles\", \"dial_code\": \"+599\", \"code\": \"AN\", \"flag\": \"🇦🇳\" }, { \"name\": \"New Caledonia\", \"dial_code\": \"+687\", \"code\": \"NC\", \"flag\": \"🇳🇨\" }, { \"name\": \"New Zealand\", \"dial_code\": \"+64\", \"code\": \"NZ\", \"flag\": \"🇳🇿\" }, { \"name\": \"Nicaragua\", \"dial_code\": \"+505\", \"code\": \"NI\", \"flag\": \"🇳🇮\" }, { \"name\": \"Niger\", \"dial_code\": \"+227\", \"code\": \"NE\", \"flag\": \"🇳🇪\" }, { \"name\": \"Nigeria\", \"dial_code\": \"+234\", \"code\": \"NG\", \"flag\": \"🇳🇬\" }, { \"name\": \"Niue\", \"dial_code\": \"+683\", \"code\": \"NU\", \"flag\": \"🇳🇺\" }, { \"name\": \"Norfolk Island\", \"dial_code\": \"+672\", \"code\": \"NF\", \"flag\": \"🇳🇫\" }, { \"name\": \"Northern Mariana Islands\", \"dial_code\": \"+1670\", \"code\": \"MP\", \"flag\": \"🇲🇵\" }, { \"name\": \"Norway\", \"dial_code\": \"+47\", \"code\": \"NO\", \"flag\": \"🇳🇴\" }, { \"name\": \"Oman\", \"dial_code\": \"+968\", \"code\": \"OM\", \"flag\": \"🇴🇲\" }, { \"name\": \"Pakistan\", \"dial_code\": \"+92\", \"code\": \"PK\", \"flag\": \"🇵🇰\" }, { \"name\": \"Palau\", \"dial_code\": \"+680\", \"code\": \"PW\", \"flag\": \"🇵🇼\" }, { \"name\": \"Palestinian Territory, Occupied\", \"dial_code\": \"+970\", \"code\": \"PS\", \"flag\": \"🇵🇸\" }, { \"name\": \"Panama\", \"dial_code\": \"+507\", \"code\": \"PA\", \"flag\": \"🇵🇦\" }, { \"name\": \"Papua New Guinea\", \"dial_code\": \"+675\", \"code\": \"PG\", \"flag\": \"🇵🇬\" }, { \"name\": \"Paraguay\", \"dial_code\": \"+595\", \"code\": \"PY\", \"flag\": \"🇵🇾\" }, { \"name\": \"Peru\", \"dial_code\": \"+51\", \"code\": \"PE\", \"flag\": \"🇵🇪\" }, { \"name\": \"Philippines\", \"dial_code\": \"+63\", \"code\": \"PH\", \"flag\": \"🇵🇭\" }, { \"name\": \"Pitcairn\", \"dial_code\": \"+872\", \"code\": \"PN\", \"flag\": \"🇵🇳\" }, { \"name\": \"Poland\", \"dial_code\": \"+48\", \"code\": \"PL\", \"flag\": \"🇵🇱\" }, { \"name\": \"Portugal\", \"dial_code\": \"+351\", \"code\": \"PT\", \"flag\": \"🇵🇹\" }, { \"name\": \"Puerto Rico\", \"dial_code\": \"+1939\", \"code\": \"PR\", \"flag\": \"🇵🇷\" }, { \"name\": \"Qatar\", \"dial_code\": \"+974\", \"code\": \"QA\", \"flag\": \"🇶🇦\" }, { \"name\": \"Romania\", \"dial_code\": \"+40\", \"code\": \"RO\", \"flag\": \"🇷🇴\" }, { \"name\": \"Russia\", \"dial_code\": \"+7\", \"code\": \"RU\", \"flag\": \"🇷🇺\" }, { \"name\": \"Rwanda\", \"dial_code\": \"+250\", \"code\": \"RW\", \"flag\": \"🇷🇼\" }, { \"name\": \"Réunion\", \"dial_code\": \"+262\", \"code\": \"RE\", \"flag\": \"🇷🇪\" }, { \"name\": \"Saint Barthélemy\", \"dial_code\": \"+590\", \"code\": \"BL\", \"flag\": \"🇧🇱\" }, { \"name\": \"Saint Helena, Ascension and Tristan Da Cunha\", \"dial_code\": \"+290\", \"code\": \"SH\", \"flag\": \"🇸🇭\" }, { \"name\": \"Saint Kitts and Nevis\", \"dial_code\": \"+1869\", \"code\": \"KN\", \"flag\": \"🇰🇳\" }, { \"name\": \"Saint Lucia\", \"dial_code\": \"+1758\", \"code\": \"LC\", \"flag\": \"🇱🇨\" }, { \"name\": \"Saint Martin\", \"dial_code\": \"+590\", \"code\": \"MF\", \"flag\": \"🇲🇫\" }, { \"name\": \"Saint Pierre and Miquelon\", \"dial_code\": \"+508\", \"code\": \"PM\", \"flag\": \"🇵🇲\" }, { \"name\": \"Saint Vincent and the Grenadines\", \"dial_code\": \"+1784\", \"code\": \"VC\", \"flag\": \"🇻🇨\" }, { \"name\": \"Samoa\", \"dial_code\": \"+685\", \"code\": \"WS\", \"flag\": \"🇼🇸\" }, { \"name\": \"San Marino\", \"dial_code\": \"+378\", \"code\": \"SM\", \"flag\": \"🇸🇲\" }, { \"name\": \"Sao Tome and Principe\", \"dial_code\": \"+239\", \"code\": \"ST\", \"flag\": \"🇸🇹\" }, { \"name\": \"Saudi Arabia\", \"dial_code\": \"+966\", \"code\": \"SA\", \"flag\": \"🇸🇦\" }, { \"name\": \"Senegal\", \"dial_code\": \"+221\", \"code\": \"SN\", \"flag\": \"🇸🇳\" }, { \"name\": \"Serbia\", \"dial_code\": \"+381\", \"code\": \"RS\", \"flag\": \"🇷🇸\" }, { \"name\": \"Seychelles\", \"dial_code\": \"+248\", \"code\": \"SC\", \"flag\": \"🇸🇨\" }, { \"name\": \"Sierra Leone\", \"dial_code\": \"+232\", \"code\": \"SL\", \"flag\": \"🇸🇱\" }, { \"name\": \"Singapore\", \"dial_code\": \"+65\", \"code\": \"SG\", \"flag\": \"🇸🇬\" }, { \"name\": \"Slovakia\", \"dial_code\": \"+421\", \"code\": \"SK\", \"flag\": \"🇸🇰\" }, { \"name\": \"Slovenia\", \"dial_code\": \"+386\", \"code\": \"SI\", \"flag\": \"🇸🇮\" }, { \"name\": \"Solomon Islands\", \"dial_code\": \"+677\", \"code\": \"SB\", \"flag\": \"🇸🇧\" }, { \"name\": \"Somalia\", \"dial_code\": \"+252\", \"code\": \"SO\", \"flag\": \"🇸🇴\" }, { \"name\": \"South Africa\", \"dial_code\": \"+27\", \"code\": \"ZA\", \"flag\": \"🇿🇦\" }, { \"name\": \"South Georgia and the South Sandwich Islands\", \"dial_code\": \"+500\", \"code\": \"GS\", \"flag\": \"🇬🇸\" }, { \"name\": \"Spain\", \"dial_code\": \"+34\", \"code\": \"ES\", \"flag\": \"🇪🇸\" }, { \"name\": \"Sri Lanka\", \"dial_code\": \"+94\", \"code\": \"LK\", \"flag\": \"🇱🇰\" }, { \"name\": \"Sudan\", \"dial_code\": \"+249\", \"code\": \"SD\", \"flag\": \"🇸🇩\" }, { \"name\": \"Suriname\", \"dial_code\": \"+597\", \"code\": \"SR\", \"flag\": \"🇸🇷\" }, { \"name\": \"Svalbard and Jan Mayen\", \"dial_code\": \"+47\", \"code\": \"SJ\", \"flag\": \"🇸🇯\" }, { \"name\": \"Swaziland\", \"dial_code\": \"+268\", \"code\": \"SZ\", \"flag\": \"🇸🇿\" }, { \"name\": \"Sweden\", \"dial_code\": \"+46\", \"code\": \"SE\", \"flag\": \"🇸🇪\" }, { \"name\": \"Switzerland\", \"dial_code\": \"+41\", \"code\": \"CH\", \"flag\": \"🇨🇭\" }, { \"name\": \"Syrian Arab Republic\", \"dial_code\": \"+963\", \"code\": \"SY\", \"flag\": \"🇸🇾\" }, { \"name\": \"Taiwan, Province of China\", \"dial_code\": \"+886\", \"code\": \"TW\", \"flag\": \"🇹🇼\" }, { \"name\": \"Tajikistan\", \"dial_code\": \"+992\", \"code\": \"TJ\", \"flag\": \"🇹🇯\" }, { \"name\": \"Tanzania, United Republic of\", \"dial_code\": \"+255\", \"code\": \"TZ\", \"flag\": \"🇹🇿\" }, { \"name\": \"Thailand\", \"dial_code\": \"+66\", \"code\": \"TH\", \"flag\": \"🇹🇭\" }, { \"name\": \"Timor-Leste\", \"dial_code\": \"+670\", \"code\": \"TL\", \"flag\": \"🇹🇱\" }, { \"name\": \"Togo\", \"dial_code\": \"+228\", \"code\": \"TG\", \"flag\": \"🇹🇬\" }, { \"name\": \"Tokelau\", \"dial_code\": \"+690\", \"code\": \"TK\", \"flag\": \"🇹🇰\" }, { \"name\": \"Tonga\", \"dial_code\": \"+676\", \"code\": \"TO\", \"flag\": \"🇹🇴\" }, { \"name\": \"Trinidad and Tobago\", \"dial_code\": \"+1868\", \"code\": \"TT\", \"flag\": \"🇹🇹\" }, { \"name\": \"Tunisia\", \"dial_code\": \"+216\", \"code\": \"TN\", \"flag\": \"🇹🇳\" }, { \"name\": \"Turkey\", \"dial_code\": \"+90\", \"code\": \"TR\", \"flag\": \"🇹🇷\" }, { \"name\": \"Turkmenistan\", \"dial_code\": \"+993\", \"code\": \"TM\", \"flag\": \"🇹🇲\" }, { \"name\": \"Turks and Caicos Islands\", \"dial_code\": \"+1649\", \"code\": \"TC\", \"flag\": \"🇹🇨\" }, { \"name\": \"Tuvalu\", \"dial_code\": \"+688\", \"code\": \"TV\", \"flag\": \"🇹🇻\" }, { \"name\": \"Uganda\", \"dial_code\": \"+256\", \"code\": \"UG\", \"flag\": \"🇺🇬\" }, { \"name\": \"Ukraine\", \"dial_code\": \"+380\", \"code\": \"UA\", \"flag\": \"🇺🇦\" }, { \"name\": \"United Arab Emirates\", \"dial_code\": \"+971\", \"code\": \"AE\", \"preferred\": true, \"flag\": \"🇦🇪\" }, { \"name\": \"United Kingdom\", \"dial_code\": \"+44\", \"code\": \"GB\", \"preferred\": true, \"flag\": \"🇬🇧\" }, { \"name\": \"United States\", \"dial_code\": \"+1\", \"code\": \"US\", \"preferred\": true, \"flag\": \"🇺🇸\" }, { \"name\": \"Uruguay\", \"dial_code\": \"+598\", \"code\": \"UY\", \"flag\": \"🇺🇾\" }, { \"name\": \"Uzbekistan\", \"dial_code\": \"+998\", \"code\": \"UZ\", \"flag\": \"🇺🇿\" }, { \"name\": \"Vanuatu\", \"dial_code\": \"+678\", \"code\": \"VU\", \"flag\": \"🇻🇺\" }, { \"name\": \"Venezuela, Bolivarian Republic of\", \"dial_code\": \"+58\", \"code\": \"VE\", \"flag\": \"🇻🇪\" }, { \"name\": \"Viet Nam\", \"dial_code\": \"+84\", \"code\": \"VN\", \"flag\": \"🇻🇳\" }, { \"name\": \"Virgin Islands, British\", \"dial_code\": \"+1284\", \"code\": \"VG\", \"flag\": \"🇻🇬\" }, { \"name\": \"Virgin Islands, U.S.\", \"dial_code\": \"+1340\", \"code\": \"VI\", \"flag\": \"🇻🇮\" }, { \"name\": \"Wallis and Futuna\", \"dial_code\": \"+681\", \"code\": \"WF\", \"flag\": \"🇼🇫\" }, { \"name\": \"Yemen\", \"dial_code\": \"+967\", \"code\": \"YE\", \"flag\": \"🇾🇪\" }, { \"name\": \"Zambia\", \"dial_code\": \"+260\", \"code\": \"ZM\", \"flag\": \"🇿🇲\" }, { \"name\": \"Zimbabwe\", \"dial_code\": \"+263\", \"code\": \"ZW\", \"flag\": \"🇿🇼\" }, { \"name\": \"Åland Islands\", \"dial_code\": \"+358\", \"code\": \"AX\", \"flag\": \"🇦🇽\" } ]" } } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/data/datatype/RequestStatus.kt b/app/src/main/java/io/xxlabs/messenger/data/datatype/RequestStatus.kt index 1f3ef037a0ef58ad376e623b4fdaebd33e97552f..45fc38d723c61ddd7425f49d9ee6465e286e5893 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/datatype/RequestStatus.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/datatype/RequestStatus.kt @@ -1,16 +1,19 @@ package io.xxlabs.messenger.data.datatype enum class RequestStatus(val value: Int) { - REJECTED(-1), SENT(0), - RECEIVED(1), + VERIFIED(1), ACCEPTED(2), SEND_FAIL(3), CONFIRM_FAIL(4), - UNVERIFIED(5), + VERIFICATION_FAIL(5), VERIFYING(6), - RESET_SENT(7), - RESET_FAIL(8); + RESET_SENT(9), + RESET_FAIL(8), + RESENT(7), + SENDING(10), + DELETING(11), + HIDDEN(12); companion object { fun from(value: Int) = values().first { it.value == value } diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/dao/ContactsDao.kt b/app/src/main/java/io/xxlabs/messenger/data/room/dao/ContactsDao.kt index 8bc3f3e7a83670881710443b1617000fd2bde083..7372cde628632a85aaa673cee0ca08e06ba30716 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/room/dao/ContactsDao.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/room/dao/ContactsDao.kt @@ -6,6 +6,7 @@ import io.reactivex.Flowable import io.reactivex.Maybe import io.reactivex.Single import io.xxlabs.messenger.data.room.model.ContactData +import kotlinx.coroutines.flow.Flow @Dao interface ContactsDao { @@ -63,6 +64,9 @@ interface ContactsDao { @Query("SELECT * FROM Contacts WHERE userId = :userId LIMIT 1") fun queryContactByUserIdForce(userId: ByteArray): Single<ContactData> + @Query("SELECT * FROM Contacts WHERE userId = :userId LIMIT 1") + fun getContactFlow(userId: ByteArray): Flow<ContactData> + @Query("SELECT * FROM Contacts WHERE id = :id LIMIT 1") fun queryContactById(id: Long): Maybe<ContactData> @@ -72,6 +76,9 @@ interface ContactsDao { @Query("UPDATE Contacts SET name = :name WHERE id = :id") fun updateContactName(id: Long, name: String): Single<Int> + @Query("UPDATE Contacts SET name = :nickname WHERE userId = :userId") + suspend fun updateContactNickname(userId: ByteArray, nickname: String): Int + @Query("UPDATE Contacts SET username = :username WHERE id = :id") fun updateContactUsername(id: Long, username: String): Single<Int> diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/dao/GroupsDao.kt b/app/src/main/java/io/xxlabs/messenger/data/room/dao/GroupsDao.kt index 3048795314f297e94489dc9942730507035b225a..78be9bd6a40c027b45dd195d9390104fa4ef89b4 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/room/dao/GroupsDao.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/room/dao/GroupsDao.kt @@ -28,6 +28,12 @@ interface GroupsDao { @Query("SELECT * FROM Groups WHERE status == 2") fun getAllAcceptedGroupsLive(): LiveData<List<GroupData>> + @Query("SELECT * FROM Groups WHERE status != 2") + suspend fun getAllGroupRequests(): List<GroupData> + @Query("UPDATE Groups SET status = 2 WHERE groupId == :groupId") fun acceptGroup(groupId: ByteArray): Single<Int> + + @Query("UPDATE Groups SET status = :status WHERE groupId = :groupId") + fun updateContactState(groupId: ByteArray, status: Int): Single<Int> } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/dao/RequestsDao.kt b/app/src/main/java/io/xxlabs/messenger/data/room/dao/RequestsDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..add64887956a79f92de3186295ebcb409cbdae68 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/data/room/dao/RequestsDao.kt @@ -0,0 +1,32 @@ +package io.xxlabs.messenger.data.room.dao + +import androidx.room.* +import io.xxlabs.messenger.data.room.model.RequestData +import kotlinx.coroutines.flow.Flow + +@Dao +interface RequestsDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(request: RequestData): Long + + @Update + suspend fun update(request: RequestData): Int + + @Delete + suspend fun delete(request: RequestData): Int + + @Query("SELECT * FROM Requests") + fun getAllRequests(): Flow<List<RequestData>> + + @Query("SELECT * FROM Requests WHERE requestId IN (SELECT userId FROM Contacts)") + fun getContactRequests(): Flow<List<RequestData>> + + @Query("SELECT * FROM Requests WHERE requestId IN (SELECT groupId FROM Groups)") + fun getGroupInvitations(): Flow<List<RequestData>> + + @Query("SELECT * FROM Requests WHERE requestId = :requestId") + suspend fun getRequest(requestId: ByteArray): List<RequestData> + + @Query("SELECT * FROM Requests WHERE unread = :unread") + fun unreadRequestsFlow(unread: Boolean = true): Flow<List<RequestData>> +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/migration/MigrationsFactory.kt b/app/src/main/java/io/xxlabs/messenger/data/room/migration/MigrationsFactory.kt index 0b3c4e54452a340995acb8e5427b73da9d387197..66925758d351316037dea62d53cd04436794994a 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/room/migration/MigrationsFactory.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/room/migration/MigrationsFactory.kt @@ -4,12 +4,28 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase object MigrationsFactory { + + /** + * Add Requests table. + */ + private val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + val createRequestsTableQuery = + "CREATE TABLE IF NOT EXISTS `Requests` " + + "(`requestId` BLOB NOT NULL, " + + "`createdAt` INTEGER NOT NULL, " + + "`unread` INTEGER NOT NULL," + + " PRIMARY KEY(`requestId`))" + database.execSQL(createRequestsTableQuery) + } + } + /** * Create an array of [Migration]s to update earlier versions * of the app database. */ fun create(): Array<Migration> { - val migrations = mutableListOf<Migration>() + val migrations = mutableListOf<Migration>(MIGRATION_1_2) return migrations.toTypedArray() } } diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/model/Contact.kt b/app/src/main/java/io/xxlabs/messenger/data/room/model/Contact.kt index 5296c459b86185896ccbd2ca17cb0220db352c10..a9f249766a1073b822d2bcb8e0475a233db86a01 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/room/model/Contact.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/room/model/Contact.kt @@ -1,6 +1,9 @@ package io.xxlabs.messenger.data.room.model -interface Contact { +import io.xxlabs.messenger.data.data.Country +import java.io.Serializable + +interface Contact : Serializable { val id: Long val userId: ByteArray val username: String @@ -17,4 +20,12 @@ interface Contact { override fun equals(other: Any?): Boolean override fun hashCode(): Int fun hasFacts(): Boolean -} \ No newline at end of file +} + +fun Contact.formattedEmail(): String? = + if (email.isNotBlank()) email.substring(1) + else null + +fun Contact.formattedPhone(flagEmoji: Boolean = false): String? = + if (phone.isNotBlank()) Country.toFormattedNumber(phone, flagEmoji) + else null diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/model/ContactData.kt b/app/src/main/java/io/xxlabs/messenger/data/room/model/ContactData.kt index ef61e6f67541589e8820ff10c66b52a774813ddd..bc484ff5fe4a9706709e37f570ed01736c22f582 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/room/model/ContactData.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/room/model/ContactData.kt @@ -5,10 +5,10 @@ import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase -import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBindings import io.xxlabs.messenger.data.datatype.RequestStatus import io.xxlabs.messenger.support.util.Utils import timber.log.Timber +import java.io.Serializable import java.util.* @Entity( diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/model/Group.kt b/app/src/main/java/io/xxlabs/messenger/data/room/model/Group.kt index 5f547780850d3ecf95f71ecbd101d73bc9337f79..f31865807db5d79de4a17a8d818e1d8e55eafa53 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/room/model/Group.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/room/model/Group.kt @@ -1,6 +1,8 @@ package io.xxlabs.messenger.data.room.model -interface Group { +import java.io.Serializable + +interface Group : Serializable { val id: Long val groupId: ByteArray val name: String diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/model/GroupData.kt b/app/src/main/java/io/xxlabs/messenger/data/room/model/GroupData.kt index 49ca0755ebdc36d807e60034885103cdfbc1c750..049ed4c295c280d51e75f0abd424aaa90e7c8549 100644 --- a/app/src/main/java/io/xxlabs/messenger/data/room/model/GroupData.kt +++ b/app/src/main/java/io/xxlabs/messenger/data/room/model/GroupData.kt @@ -4,6 +4,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey +import io.xxlabs.messenger.bindings.wrapper.groups.group.GroupBase import io.xxlabs.messenger.data.datatype.RequestStatus @Entity( @@ -45,4 +46,15 @@ data class GroupData( result = 31 * result + status.hashCode() return result } + + companion object { + fun from(groupBindings: GroupBase, status: RequestStatus): GroupData = + GroupData( + groupId = groupBindings.getID(), + name = groupBindings.getName().decodeToString(), + leader = groupBindings.getMembership()[0].getID(), + serial = groupBindings.serialize(), + status = status.value + ) + } } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/model/RequestData.kt b/app/src/main/java/io/xxlabs/messenger/data/room/model/RequestData.kt new file mode 100644 index 0000000000000000000000000000000000000000..5641fba23d6b680d78d6f32c1b6163d85f9c42f5 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/data/room/model/RequestData.kt @@ -0,0 +1,28 @@ +package io.xxlabs.messenger.data.room.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import io.xxlabs.messenger.requests.model.Request + +@Entity(tableName = "Requests") +data class RequestData( + @PrimaryKey + @ColumnInfo(name = "requestId", typeAffinity = ColumnInfo.BLOB) + val requestId: ByteArray = byteArrayOf(), + @ColumnInfo(name = "createdAt", typeAffinity = ColumnInfo.INTEGER) + val createdAt: Long = 0, + @ColumnInfo(name = "unread") + val unread: Boolean = true +) { + + companion object { + fun from(request: Request): RequestData = + RequestData( + requestId = request.requestId, + createdAt = System.currentTimeMillis(), + unread = request.unread + ) + } +} diff --git a/app/src/main/java/io/xxlabs/messenger/di/modules/AppModule.kt b/app/src/main/java/io/xxlabs/messenger/di/modules/AppModule.kt index c48f6fbcff8c7f0012c5ffcc2f6b2fbfbd6b8e9d..e56a2bce91a360c15e195e45e069c97a1c0004e6 100644 --- a/app/src/main/java/io/xxlabs/messenger/di/modules/AppModule.kt +++ b/app/src/main/java/io/xxlabs/messenger/di/modules/AppModule.kt @@ -8,14 +8,14 @@ import io.xxlabs.messenger.application.AppDatabase import io.xxlabs.messenger.application.AppRxSchedulers import io.xxlabs.messenger.application.SchedulerProvider import io.xxlabs.messenger.backup.BackupModule -import io.xxlabs.messenger.backup.bindings.BindingsBackupMediator -import io.xxlabs.messenger.backup.data.BackupRepository +import io.xxlabs.messenger.backup.bindings.BackupService import io.xxlabs.messenger.bindings.listeners.MessageReceivedListener import io.xxlabs.messenger.repository.DaoRepository import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.repository.base.BaseRepository import io.xxlabs.messenger.repository.client.ClientRepository import io.xxlabs.messenger.repository.mock.ClientMockRepository +import io.xxlabs.messenger.requests.RequestsModule import io.xxlabs.messenger.support.isMockVersion import io.xxlabs.messenger.ui.main.settings.SettingsModule import javax.inject.Singleton @@ -24,6 +24,7 @@ import javax.inject.Singleton ViewModelModule::class, SettingsModule::class, BackupModule::class, + RequestsModule::class ]) class AppModule { @Provides @@ -47,7 +48,7 @@ class AppModule { daoRepo: DaoRepository, preferencesRepository: PreferencesRepository, messageReceivedListener: MessageReceivedListener, - backupService: BackupRepository + backupService: BackupService ): BaseRepository { return if (isMockVersion()) { ClientMockRepository(preferencesRepository) diff --git a/app/src/main/java/io/xxlabs/messenger/di/modules/FragmentMainBuildersModule.kt b/app/src/main/java/io/xxlabs/messenger/di/modules/FragmentMainBuildersModule.kt index e71ea813ec5151533eb14c53dd9306e1eaaf0faf..093b915edb80d6b40fb05d6476f85f7745da7337 100644 --- a/app/src/main/java/io/xxlabs/messenger/di/modules/FragmentMainBuildersModule.kt +++ b/app/src/main/java/io/xxlabs/messenger/di/modules/FragmentMainBuildersModule.kt @@ -3,9 +3,10 @@ package io.xxlabs.messenger.di.modules import dagger.Module import dagger.android.ContributesAndroidInjector import io.xxlabs.messenger.backup.ui.list.BackupListFragment -import io.xxlabs.messenger.backup.ui.save.BackupDetailFragment -import io.xxlabs.messenger.backup.ui.save.BackupSettingsFragment +import io.xxlabs.messenger.backup.ui.backup.BackupDetailFragment +import io.xxlabs.messenger.backup.ui.backup.BackupSettingsFragment import io.xxlabs.messenger.media.FullScreenImageFragment +import io.xxlabs.messenger.requests.deprecated.RequestGenericFragment import io.xxlabs.messenger.ui.main.chat.PrivateMessagesFragment import io.xxlabs.messenger.ui.main.chats.ChatsFragment import io.xxlabs.messenger.ui.main.contacts.ContactsFragment @@ -18,8 +19,16 @@ import io.xxlabs.messenger.ui.main.groups.GroupMessagesFragment import io.xxlabs.messenger.ui.main.qrcode.QrCodeFragment import io.xxlabs.messenger.ui.main.qrcode.QrCodeScanFragment import io.xxlabs.messenger.ui.main.qrcode.QrCodeShowFragment -import io.xxlabs.messenger.ui.main.requests.RequestGenericFragment -import io.xxlabs.messenger.ui.main.requests.RequestsFragment +import io.xxlabs.messenger.requests.ui.RequestsFragment +import io.xxlabs.messenger.requests.ui.accepted.contact.RequestAcceptedDialog +import io.xxlabs.messenger.requests.ui.accepted.group.InvitationAcceptedDialog +import io.xxlabs.messenger.requests.ui.details.contact.RequestDetailsDialog +import io.xxlabs.messenger.requests.ui.details.group.InvitationDetailsDialog +import io.xxlabs.messenger.requests.ui.list.FailedRequestsFragment +import io.xxlabs.messenger.requests.ui.list.ReceivedRequestsFragment +import io.xxlabs.messenger.requests.ui.list.SentRequestsFragment +import io.xxlabs.messenger.requests.ui.nickname.SaveNicknameDialog +import io.xxlabs.messenger.requests.ui.send.SendRequestDialog import io.xxlabs.messenger.ui.main.settings.DeleteAccountFragment import io.xxlabs.messenger.ui.main.settings.SettingsFragment import io.xxlabs.messenger.ui.main.settings.SettingsAdvancedFragment @@ -98,4 +107,31 @@ abstract class FragmentMainBuildersModule { @ContributesAndroidInjector abstract fun contributeBackupSettingsFragment(): BackupSettingsFragment + + @ContributesAndroidInjector + abstract fun contributeSentRequestsFragment(): SentRequestsFragment + + @ContributesAndroidInjector + abstract fun contributeReceivedRequestsFragment(): ReceivedRequestsFragment + + @ContributesAndroidInjector + abstract fun contributeFailedRequestsFragment(): FailedRequestsFragment + + @ContributesAndroidInjector + abstract fun contributeRequestDetailsDialog(): RequestDetailsDialog + + @ContributesAndroidInjector + abstract fun contributeRequestAcceptedDialog(): RequestAcceptedDialog + + @ContributesAndroidInjector + abstract fun contributeInvitationDetailsDialog(): InvitationDetailsDialog + + @ContributesAndroidInjector + abstract fun contributeSendRequestDialog(): SendRequestDialog + + @ContributesAndroidInjector + abstract fun contributeSaveNicknameDialog(): SaveNicknameDialog + + @ContributesAndroidInjector + abstract fun contributeInvitationAcceptedDialog(): InvitationAcceptedDialog } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/di/modules/RoomModule.kt b/app/src/main/java/io/xxlabs/messenger/di/modules/RoomModule.kt index c9a1ffd3e7d023cc796497f600c1b8253e32fd9b..0efbfd34c3f4278c51825ff69902b56e4b7f9f5f 100644 --- a/app/src/main/java/io/xxlabs/messenger/di/modules/RoomModule.kt +++ b/app/src/main/java/io/xxlabs/messenger/di/modules/RoomModule.kt @@ -5,6 +5,7 @@ import dagger.Provides import io.xxlabs.messenger.application.AppDatabase import io.xxlabs.messenger.data.room.dao.ContactsDao import io.xxlabs.messenger.data.room.dao.MessagesDao +import io.xxlabs.messenger.data.room.dao.RequestsDao import javax.inject.Singleton @@ -17,4 +18,8 @@ class RoomModule { @Singleton @Provides fun provideMessagesDao(database: AppDatabase): MessagesDao = database.messagesDao() + + @Singleton + @Provides + fun provideRequestsDao(database: AppDatabase): RequestsDao = database.requestsDao() } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/di/modules/ViewModelModule.kt b/app/src/main/java/io/xxlabs/messenger/di/modules/ViewModelModule.kt index 1dd5cc4936c5cd6ee3c3f0825882029ddf32c49a..2ce2ecfb189b75c91dbfbbe00752491c43bb137c 100644 --- a/app/src/main/java/io/xxlabs/messenger/di/modules/ViewModelModule.kt +++ b/app/src/main/java/io/xxlabs/messenger/di/modules/ViewModelModule.kt @@ -7,6 +7,7 @@ import dagger.Module import dagger.multibindings.IntoMap import io.xxlabs.messenger.di.utils.DaggerViewModelFactory import io.xxlabs.messenger.di.utils.ViewModelKey +import io.xxlabs.messenger.requests.ui.RequestsViewModel import io.xxlabs.messenger.ui.base.ContactDetailsViewModel import io.xxlabs.messenger.ui.global.ContactsViewModel import io.xxlabs.messenger.ui.global.NetworkViewModel @@ -85,6 +86,11 @@ abstract class ViewModelModule { @ViewModelKey(QrCodeViewModel::class) abstract fun bindQrCodeViewModel(qrCodeViewModel: QrCodeViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(RequestsViewModel::class) + abstract fun bindRequestsViewModel(requestsViewModel: RequestsViewModel): ViewModel + @Binds abstract fun bindViewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/filetransfer/FileTransferManager.kt b/app/src/main/java/io/xxlabs/messenger/filetransfer/FileTransferManager.kt index f35a6cb0c6af942eea8967cadca48424d616b530..69eafa93cbba4b7e48ec374380e75d0aab8b4a84 100644 --- a/app/src/main/java/io/xxlabs/messenger/filetransfer/FileTransferManager.kt +++ b/app/src/main/java/io/xxlabs/messenger/filetransfer/FileTransferManager.kt @@ -179,7 +179,7 @@ class FileTransferManager(repo: BaseRepository) : FileTransferRepository { @Throws(Exception::class) override fun resend(transferId: TransferId) { // Removed from latest bindings -// manager.resend(transferId.tid) +// manager.send(transferId.tid) } @Throws(Exception::class) diff --git a/app/src/main/java/io/xxlabs/messenger/repository/DaoRepository.kt b/app/src/main/java/io/xxlabs/messenger/repository/DaoRepository.kt index f44d9549af76474450dbbc8d53b7647e3a518cc7..391e26400896205efd86551dd03d5cddd4c5a1a9 100644 --- a/app/src/main/java/io/xxlabs/messenger/repository/DaoRepository.kt +++ b/app/src/main/java/io/xxlabs/messenger/repository/DaoRepository.kt @@ -5,6 +5,7 @@ import androidx.paging.Config import androidx.paging.DataSource import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList +import com.airbnb.lottie.L import io.reactivex.Flowable import io.reactivex.Maybe import io.reactivex.Notification @@ -19,6 +20,7 @@ import io.xxlabs.messenger.data.datatype.RequestStatus import io.xxlabs.messenger.data.datatype.MessageStatus import io.xxlabs.messenger.data.room.model.* import io.xxlabs.messenger.support.isMockVersion +import kotlinx.coroutines.flow.Flow import timber.log.Timber import javax.inject.Inject @@ -223,6 +225,10 @@ class DaoRepository @Inject constructor( return contactsDao.updateContactName(temporaryContact.id, temporaryContact.nickname) } + suspend fun updateContactNickname(contact: ContactData): Int = + contactsDao.updateContactNickname(contact.userId, contact.nickname) + + fun searchContactByUsernameLikeness(username: String): Single<List<ContactData>> { return contactsDao.queryAllContactsUsername(username) } @@ -239,6 +245,10 @@ class DaoRepository @Inject constructor( return contactsDao.updateContactState(userId, requestStatus.value) } + fun updateGroupState(groupId: ByteArray, requestStatus: RequestStatus): Single<Int> { + return groupsDao.updateContactState(groupId, requestStatus.value) + } + fun getContactById(id: Long): Maybe<ContactData> { return contactsDao.queryContactById(id) } @@ -251,6 +261,9 @@ class DaoRepository @Inject constructor( return contactsDao.queryContactByUserId(userId) } + fun getContactFlow(userId: ByteArray): Flow<ContactData> = + contactsDao.getContactFlow(userId) + fun getContactByUsername(username: String): Maybe<ContactData> { return contactsDao.queryContactByUsername(username) } @@ -325,7 +338,7 @@ class DaoRepository @Inject constructor( name = group.getName().decodeToString(), leader = group.getMembership()[1].getID(), serial = group.serialize(), - status = RequestStatus.RECEIVED.value + status = RequestStatus.VERIFIED.value ) } else { GroupData( @@ -459,7 +472,7 @@ class DaoRepository @Inject constructor( return groupsDao.deleteGroup(groupData) } - fun acceptGroup(group: GroupData): Single<Int> { + fun acceptGroup(group: Group): Single<Int> { return groupsDao.acceptGroup(group.groupId) } @@ -475,6 +488,8 @@ class DaoRepository @Inject constructor( return groupsDao.getAllAcceptedGroupsLive() } + suspend fun getAllGroupRequests() = groupsDao.getAllGroupRequests() + fun deleteAllGroupMessages(): Single<Int> { return groupMessagesDao.deleteAllMessages() } diff --git a/app/src/main/java/io/xxlabs/messenger/repository/PreferencesRepository.kt b/app/src/main/java/io/xxlabs/messenger/repository/PreferencesRepository.kt index b6ee82c39153b69a5a45de187ac5567f1be59edb..83b4f1a7ea8b2b493e59ca9d9d299f56d4062735 100644 --- a/app/src/main/java/io/xxlabs/messenger/repository/PreferencesRepository.kt +++ b/app/src/main/java/io/xxlabs/messenger/repository/PreferencesRepository.kt @@ -12,12 +12,14 @@ import io.xxlabs.messenger.repository.base.BasePreferences import io.xxlabs.messenger.support.extensions.fromBase64toByteArray import io.xxlabs.messenger.support.extensions.toBase64String import io.xxlabs.messenger.support.util.Utils -import io.xxlabs.messenger.ui.main.requests.RequestsFilter +import io.xxlabs.messenger.requests.deprecated.RequestsFilter import timber.log.Timber import java.security.KeyStore import javax.inject.Inject -class PreferencesRepository @Inject constructor(context: Context) : BasePreferences() { +class PreferencesRepository @Inject constructor( + context: Context +) : BasePreferences() { private val masterKeyAlias = "xx_preferences_key" private val preferencesAlias = "xx_preferences" private val masterKeySpec = KeyGenParameterSpec.Builder( @@ -200,6 +202,12 @@ class PreferencesRepository @Inject constructor(context: Context) : BasePreferen registrationStep += 1 } + override var isFirstLaunch: Boolean + get() = preferences.getBoolean("is_first_launch", true) + set(value) { + preferences.edit().putBoolean("is_first_launch", value).apply() + } + override var isFirstTimeNotifications: Boolean get() = preferences.getBoolean("is_first_time_notifications", true) set(value) { @@ -246,13 +254,13 @@ class PreferencesRepository @Inject constructor(context: Context) : BasePreferen preferences.edit().putInt("registration_step", value).apply() } - override var shouldShareEmailQr: Boolean + override var shareEmailWhenRequesting: Boolean get() = preferences.getBoolean("should_share_email_qr", false) set(value) { preferences.edit().putBoolean("should_share_email_qr", value).apply() } - override var shouldSharePhoneQr: Boolean + override var sharePhoneWhenRequesting: Boolean get() = preferences.getBoolean("should_share_phone_qr", false) set(value) { preferences.edit().putBoolean("should_share_phone_qr", value).apply() diff --git a/app/src/main/java/io/xxlabs/messenger/repository/base/BasePreferences.kt b/app/src/main/java/io/xxlabs/messenger/repository/base/BasePreferences.kt index 7be3d728c86f407e008e1e36ec97484b85ce33c8..8acd1105d013ae9e2d2236c4019159f964ae98e0 100644 --- a/app/src/main/java/io/xxlabs/messenger/repository/base/BasePreferences.kt +++ b/app/src/main/java/io/xxlabs/messenger/repository/base/BasePreferences.kt @@ -1,9 +1,10 @@ package io.xxlabs.messenger.repository.base +import io.xxlabs.messenger.backup.data.backup.BackupPreferencesRepository import io.xxlabs.messenger.data.data.ContactRoundRequest -import io.xxlabs.messenger.ui.main.requests.RequestsFilter +import io.xxlabs.messenger.requests.deprecated.RequestsFilter -abstract class BasePreferences { +abstract class BasePreferences : BackupPreferencesRepository { abstract fun addContactRequest(contactRoundRequest: ContactRoundRequest) abstract fun addContactRequest(contactId: ByteArray, contactUsername: String, roundId: Long, isSent: Boolean) abstract fun removeContactRequest(contactRoundRequest: ContactRoundRequest) @@ -17,14 +18,15 @@ abstract class BasePreferences { abstract fun removeContactRequests(contactId: ByteArray): Int //User + abstract var isFirstLaunch: Boolean abstract var isFirstTimeNotifications: Boolean abstract var isFirstTimeCoverMessages: Boolean abstract var preImages: String abstract var userData: String abstract var userPicture: String abstract var userSecret: String - abstract var shouldShareEmailQr: Boolean - abstract var shouldSharePhoneQr: Boolean + abstract var shareEmailWhenRequesting: Boolean + abstract var sharePhoneWhenRequesting: Boolean abstract var registrationStep: Int //General @@ -50,15 +52,4 @@ abstract class BasePreferences { //Other abstract var contactRoundRequests: MutableSet<String> - - //Account Backup - abstract val isBackupEnabled: Boolean - abstract val isGoogleDriveEnabled: Boolean - abstract val isDropboxEnabled: Boolean - abstract var backupPassword: String? - abstract var autoBackup: Boolean - abstract var wiFiOnlyBackup: Boolean - abstract var backupLocation: String? - abstract var dbxCredential: String? - abstract var isUserProfileBackedUp: Boolean } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt b/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt index f8472fa31285bc85c7c3d1fa8f586beee56743a3..f943c3a8417a32d8d5d8b4e24d54d351678b3d14 100644 --- a/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt +++ b/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt @@ -335,13 +335,13 @@ class ClientRepository @Inject constructor( userContact.addUsername(factsHash[FactType.USERNAME] ?: "") Timber.d("[CLIENT REPO] Facts: ${userContact.getStringifiedFacts()}") - if (preferences.shouldShareEmailQr) { + if (preferences.shareEmailWhenRequesting) { factsHash[FactType.EMAIL]?.let { email -> userContact.addEmail(email) } } - if (preferences.shouldSharePhoneQr) { + if (preferences.sharePhoneWhenRequesting) { factsHash[FactType.PHONE]?.let { phone -> userContact.addPhone(phone) } diff --git a/app/src/main/java/io/xxlabs/messenger/repository/mock/ClientMockRepository.kt b/app/src/main/java/io/xxlabs/messenger/repository/mock/ClientMockRepository.kt index 7564dd596e9fe1938d24852b416f62b239b18b1e..26eb71bdcdfe0068eaf6f5f6ade4a6a49fcf56ea 100644 --- a/app/src/main/java/io/xxlabs/messenger/repository/mock/ClientMockRepository.kt +++ b/app/src/main/java/io/xxlabs/messenger/repository/mock/ClientMockRepository.kt @@ -271,7 +271,7 @@ class ClientMockRepository( } input.contains("_RECEIVED") -> { cleanInput = input.substringBefore("_RECEIVED") - RequestStatus.RECEIVED + RequestStatus.VERIFIED } input.contains("_FAILED") -> { cleanInput = input.substringBefore("_FAILED") @@ -337,7 +337,7 @@ class ClientMockRepository( input.contains("_RECEIVED") -> { cleanInput = input.substringBefore("_RECEIVED") - RequestStatus.RECEIVED + RequestStatus.VERIFIED } input.contains("_FAILED") -> { diff --git a/app/src/main/java/io/xxlabs/messenger/requests/RequestsModule.kt b/app/src/main/java/io/xxlabs/messenger/requests/RequestsModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..ea33d48a0da5d78263e41930bc80c36c26ed87a4 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/RequestsModule.kt @@ -0,0 +1,32 @@ +package io.xxlabs.messenger.requests + +import dagger.Binds +import dagger.Module +import io.xxlabs.messenger.requests.bindings.BindingsInvitationsMediator +import io.xxlabs.messenger.requests.bindings.BindingsRequestMediator +import io.xxlabs.messenger.requests.bindings.GroupInvitationsService +import io.xxlabs.messenger.requests.bindings.ContactRequestsService +import io.xxlabs.messenger.requests.data.LocalRequestsDataSource +import io.xxlabs.messenger.requests.data.RequestsDatabase +import javax.inject.Singleton + +@Module +interface RequestsModule { + @Singleton + @Binds + fun localDataSource( + database: RequestsDatabase + ): LocalRequestsDataSource + + @Singleton + @Binds + fun remoteRequestsDataSource( + remote: BindingsRequestMediator + ): ContactRequestsService + + @Singleton + @Binds + fun remoteInvitationsDataSource( + remote: BindingsInvitationsMediator + ): GroupInvitationsService +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/bindings/BindingsInvitationsMediator.kt b/app/src/main/java/io/xxlabs/messenger/requests/bindings/BindingsInvitationsMediator.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a532575fdfad7156e5ed5d83d83f116e937e2a6 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/bindings/BindingsInvitationsMediator.kt @@ -0,0 +1,43 @@ +package io.xxlabs.messenger.requests.bindings + +import io.xxlabs.messenger.bindings.wrapper.groups.report.NewGroupReportBase +import io.xxlabs.messenger.data.room.model.ContactData +import io.xxlabs.messenger.data.room.model.Group +import io.xxlabs.messenger.repository.base.BaseRepository +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.support.util.value +import javax.inject.Inject + +class BindingsInvitationsMediator @Inject constructor( + private val repo: BaseRepository +) : GroupInvitationsService { + + override suspend fun createGroup( + name: String, + members: List<ContactData>, + initialMessage: String? + ): NewGroupReportBase? { + val memberIds = members.map { it.userId } + return try { + repo.makeGroup( + name, + memberIds, + initialMessage + ).value() + } catch (e: Exception) { + null + } + } + + override suspend fun joinGroup(group: GroupInvitation): Boolean { + return true + } + + override suspend fun leaveGroup(group: Group): Boolean { + return true + } + + override suspend fun resendInvitation(group: GroupInvitation): Boolean { + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/bindings/BindingsRequestMediator.kt b/app/src/main/java/io/xxlabs/messenger/requests/bindings/BindingsRequestMediator.kt new file mode 100644 index 0000000000000000000000000000000000000000..55c759e9d05b153ca2a8bd118f75497c9a3273f5 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/bindings/BindingsRequestMediator.kt @@ -0,0 +1,79 @@ +package io.xxlabs.messenger.requests.bindings + +import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.repository.base.BaseRepository +import io.xxlabs.messenger.repository.client.ClientRepository +import io.xxlabs.messenger.requests.model.ContactRequest +import io.xxlabs.messenger.support.util.value +import timber.log.Timber +import javax.inject.Inject + +class BindingsRequestMediator @Inject constructor( + private val repo: BaseRepository +): ContactRequestsService { + + override suspend fun acceptContactRequest(request: ContactRequest): Boolean = + confirmAuthenticatedChannel(request.model) + + private suspend fun confirmAuthenticatedChannel(contact: Contact): Boolean { + var result = false + unmarshalContact(contact)?.let { bindingsData -> + try { + repo.confirmAuthenticatedChannel(bindingsData).value() + result = true + } catch (e: Exception) { + val logMsg = + "Exception occurred when accepting the " + + "request from ${contact.displayName}: ${e.message}." + Timber.d(logMsg) + } + } + return result + } + + private fun unmarshalContact(contact: Contact): ByteArray? { + var contactWrapper: ContactWrapperBase? = null + + contact.marshaled?.let { marshalled -> + contactWrapper = repo.unmarshallContact(marshalled) + } + return contactWrapper?.marshal() + } + + override suspend fun sendContactRequest(request: ContactRequest): Boolean = + requestAuthenticatedChannel(request.model) + + private suspend fun requestAuthenticatedChannel(contact: Contact): Boolean { + var result = false + unmarshalContact(contact)?.let { bindingsData -> + try { + repo.requestAuthenticatedChannel(bindingsData).value() + result = true + } catch (e: Exception) { + val logMsg = + "Exception occurred when sending the " + + "request to ${contact.displayName}: ${e.message}." + Timber.d(logMsg) + } + } + return result + } + + override fun resetSession(contact: Contact): Boolean { + var result = false + try { + val roundId = ClientRepository.clientWrapper.client.resetSession( + contact.marshaled, + repo.getMashalledUser(), + "" + ) + result = roundId > 0 + } catch (e: Exception) { + Timber.d( + "Exception occurred when resetting ${contact.displayName}: ${e.message}." + ) + } + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/bindings/ContactRequestsService.kt b/app/src/main/java/io/xxlabs/messenger/requests/bindings/ContactRequestsService.kt new file mode 100644 index 0000000000000000000000000000000000000000..6995667925d4128f08cb5e103c369c965c6df5e2 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/bindings/ContactRequestsService.kt @@ -0,0 +1,10 @@ +package io.xxlabs.messenger.requests.bindings + +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.requests.model.ContactRequest + +interface ContactRequestsService { + suspend fun acceptContactRequest(request: ContactRequest): Boolean + suspend fun sendContactRequest(request: ContactRequest): Boolean + fun resetSession(contact: Contact): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/bindings/GroupInvitationsService.kt b/app/src/main/java/io/xxlabs/messenger/requests/bindings/GroupInvitationsService.kt new file mode 100644 index 0000000000000000000000000000000000000000..92c09e91c794ec8968093713931dd8c1ca87d807 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/bindings/GroupInvitationsService.kt @@ -0,0 +1,18 @@ +package io.xxlabs.messenger.requests.bindings + +import io.xxlabs.messenger.bindings.wrapper.groups.report.NewGroupReportBase +import io.xxlabs.messenger.data.room.model.ContactData +import io.xxlabs.messenger.data.room.model.Group +import io.xxlabs.messenger.requests.model.GroupInvitation + +interface GroupInvitationsService { + // TODO: Return a Result<NewGroupReportBase> + suspend fun createGroup( + name: String, + members: List<ContactData>, + initialMessage: String? + ): NewGroupReportBase? + suspend fun joinGroup(group: GroupInvitation): Boolean + suspend fun leaveGroup(group: Group): Boolean + suspend fun resendInvitation(group: GroupInvitation): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/LocalRequestsDataSource.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/LocalRequestsDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..e30a49ec10a6e5ddb133b9a4b899f107f3f26074 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/LocalRequestsDataSource.kt @@ -0,0 +1,18 @@ +package io.xxlabs.messenger.requests.data + +import io.xxlabs.messenger.data.room.model.RequestData +import io.xxlabs.messenger.requests.model.ContactRequest +import io.xxlabs.messenger.requests.model.GroupInvitation +import kotlinx.coroutines.flow.Flow + +interface LocalRequestsDataSource { + val unreadCount: Flow<Int> + + suspend fun getContactRequests(): Flow<List<RequestData>> + suspend fun getGroupInvitations(): Flow<List<RequestData>> + suspend fun getRequest(requestId: ByteArray): RequestData? + + fun addRequest(request: RequestData) + fun updateRequest(request: RequestData) + fun deleteRequest(request: RequestData) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/RequestDataSource.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/RequestDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..f0965e5639560fc4b654c0ea515d758a62cc7ca6 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/RequestDataSource.kt @@ -0,0 +1,16 @@ +package io.xxlabs.messenger.requests.data + +import io.xxlabs.messenger.requests.model.Request +import kotlinx.coroutines.flow.Flow + +interface RequestDataSource<T: Request> { + val unreadCount: Flow<Int> + suspend fun getRequests(): Flow<List<T>> + fun save(request: T) + fun markAsSeen(request: T) + suspend fun accept(request: T): Boolean + fun reject(request: T) + fun delete(request: T) + fun send(request: T) + fun resetResentRequests() +} diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/RequestsDatabase.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/RequestsDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..71643ddee64be1dc029ce403211557573d00ee28 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/RequestsDatabase.kt @@ -0,0 +1,80 @@ +package io.xxlabs.messenger.requests.data + +import io.xxlabs.messenger.BuildConfig +import io.xxlabs.messenger.application.AppDatabase +import io.xxlabs.messenger.data.room.model.RequestData +import io.xxlabs.messenger.support.extensions.toBase64String +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +class RequestsDatabase @Inject constructor( + val db: AppDatabase +) : LocalRequestsDataSource { + private val requestsDao = db.requestsDao() + + private val scope = CoroutineScope( + CoroutineName("RequestsDB") + + Job() + + Dispatchers.IO + ) + + override val unreadCount: Flow<Int> by ::_unreadCount + private val _unreadCount: MutableStateFlow<Int> = MutableStateFlow(0) + + init { + updateUnreadCount() + if (BuildConfig.DEBUG) listRequests() + } + + private fun listRequests() { + scope.launch { + requestsDao.getAllRequests().collect { requests -> + for (request in requests) { + Timber.d("Found request: ${request.requestId.toBase64String()}") + } + } + + getContactRequests().collect { contactRequests -> + for (request in contactRequests) { + Timber.d("Found contact request: ${request.requestId.toBase64String()}") + } + } + } + } + + private fun updateUnreadCount() { + scope.launch { + requestsDao.unreadRequestsFlow().collectLatest { + _unreadCount.emit(it.count()) + } + } + } + + override suspend fun getContactRequests(): Flow<List<RequestData>> = + requestsDao.getContactRequests() + .stateIn(scope, SharingStarted.Eagerly, listOf()) + + override suspend fun getGroupInvitations(): Flow<List<RequestData>> = + requestsDao.getGroupInvitations() + .stateIn(scope, SharingStarted.Eagerly, listOf()) + + + override fun addRequest(request: RequestData) { + scope.launch { requestsDao.insert(request) } + } + + override fun updateRequest(request: RequestData) { + scope.launch { requestsDao.update(request) } + } + + override fun deleteRequest(request: RequestData) { + scope.launch { requestsDao.delete(request) } + } + + override suspend fun getRequest(requestId: ByteArray): RequestData? = + withContext(scope.coroutineContext) { + requestsDao.getRequest(requestId).firstOrNull() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/contact/ContactRequest.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/contact/ContactRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a7b39630afa8fc88b737d9ed99b6b016a855775 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/contact/ContactRequest.kt @@ -0,0 +1,18 @@ +package io.xxlabs.messenger.requests.data.contact + +import io.xxlabs.messenger.data.datatype.RequestStatus +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.requests.model.ContactRequest + +/** + * Wrapper class for presenting a [Contact] as a friend request. + */ +data class ContactRequestData( + override val model: Contact, + override val unread: Boolean = false, +) : ContactRequest, Contact by model { + override val requestId: ByteArray = model.userId + override val name: String = model.displayName + override val createdAt: Long = model.createdAt + override val requestStatus: RequestStatus = RequestStatus.from(model.status) +} diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/contact/ContactRequestRepository.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/contact/ContactRequestRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..393c873c13cd3faef9fab80d4e84d2ab1aa1f9a0 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/contact/ContactRequestRepository.kt @@ -0,0 +1,124 @@ +package io.xxlabs.messenger.requests.data.contact + +import io.xxlabs.messenger.data.datatype.RequestStatus +import io.xxlabs.messenger.data.datatype.RequestStatus.* +import io.xxlabs.messenger.data.room.model.RequestData +import io.xxlabs.messenger.repository.DaoRepository +import io.xxlabs.messenger.requests.bindings.ContactRequestsService +import io.xxlabs.messenger.requests.data.LocalRequestsDataSource +import io.xxlabs.messenger.requests.data.RequestDataSource +import io.xxlabs.messenger.requests.model.ContactRequest +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.support.util.value +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +class ContactRequestsRepository @Inject constructor( + private val daoRepository: DaoRepository, + private val localDataSource: LocalRequestsDataSource, + private val requestsService: ContactRequestsService +) : RequestDataSource<ContactRequest> { + + private val scope = CoroutineScope( + CoroutineName("ContactRequestsRepo") + + Job() + + Dispatchers.IO + ) + + override val unreadCount: Flow<Int> = localDataSource.unreadCount + + override suspend fun getRequests(): Flow<List<ContactRequest>> = + localDataSource.getContactRequests().map { requestDataList -> + requestDataList.mapNotNull { requestData -> + val contactData = daoRepository + .getContactByUserId(requestData.requestId) + .value() + contactData?.let { + ContactRequestData(it, requestData.unread) + } + } + } + + override fun save(request: ContactRequest) { + localDataSource.addRequest(RequestData.from(request)) + } + + override fun markAsSeen(request: ContactRequest) { + scope.launch { + localDataSource.getRequest(request.requestId) + ?.copy(unread = false) + ?.run { + localDataSource.updateRequest(this) + } + } + } + + override suspend fun accept(request: ContactRequest): Boolean = withContext(Dispatchers.IO) { + if (requestsService.acceptContactRequest(request)) { + delete(request) + true + } else { + update(request, CONFIRM_FAIL) + false + } + } + + override fun delete(request: ContactRequest) { + scope.launch { + localDataSource.getRequest(request.requestId)?.apply { + localDataSource.deleteRequest(this) + } + } + } + + private fun update(request: ContactRequest, status: RequestStatus) { + scope.launch { + daoRepository.updateContactState(request.model.userId, status).value() + } + } + + override fun reject(request: ContactRequest) { + update(request, HIDDEN) + } + + override fun send(request: ContactRequest) { + scope.launch { + when (request.requestStatus) { + RESET_FAIL, RESET_SENT -> resetSession(request) + SENT -> resendRequest(request) + CONFIRM_FAIL -> accept(request) + SENDING -> sendRequest(request) + } + } + } + + private fun resetSession(request: ContactRequest) { + if (requestsService.resetSession(request.model)) update(request, RESET_SENT) + else update(request, RESET_FAIL) + } + + private suspend fun sendRequest(request: ContactRequest) { + if (requestsService.sendContactRequest(request)) update(request, SENT) + else update(request, SEND_FAIL) + } + + private suspend fun resendRequest(request: ContactRequest) { + if (requestsService.sendContactRequest(request)) { + update(request, RESENT) + } else update(request, SEND_FAIL) + } + + override fun resetResentRequests() { + scope.launch { + getRequests().take(2).collectLatest { requestsList -> + val resentList = requestsList.filter { + it.requestStatus == RESENT + } + for (resent in resentList) { + update(resent, SENT) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/contact/RequestMigrator.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/contact/RequestMigrator.kt new file mode 100644 index 0000000000000000000000000000000000000000..c5fc10c23606c878dd0e6c95c39251262ab873d6 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/contact/RequestMigrator.kt @@ -0,0 +1,53 @@ +package io.xxlabs.messenger.requests.data.contact + +import io.xxlabs.messenger.data.data.ContactRoundRequest +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.repository.DaoRepository +import io.xxlabs.messenger.repository.PreferencesRepository +import io.xxlabs.messenger.support.util.value +import kotlinx.coroutines.* +import timber.log.Timber + +object RequestMigrator { + + /** + * Moves requests previously saved to [PreferencesRepository] as + * [ContactRoundRequest]s to the [ContactRequestsRepository] as + * [ContactRequestData]. + */ + suspend fun performMigration( + preferences: PreferencesRepository, + requestsDataSource: ContactRequestsRepository, + daoRepository: DaoRepository + ) = withContext(Dispatchers.IO) { + val requestsToMigrate = ContactRoundRequest.toRoundRequestsSet( + preferences.contactRoundRequests + ) + for (oldRequest in requestsToMigrate) { + launch { + val contact = getContact(oldRequest, daoRepository) + contact?.let { + requestsDataSource.save(ContactRequestData(contact, false)) + preferences.removeRequest(oldRequest) + } + } + } + } + + private suspend fun getContact( + oldRequest: ContactRoundRequest, + daoRepository: DaoRepository + ): Contact? { + return try { + daoRepository.getContactByUserId(oldRequest.contactId).value() + } catch (e: Exception) { + Timber.d("An error occured during request migration: ${e.message}") + null + } + } + + private fun PreferencesRepository.removeRequest(oldRequest: ContactRoundRequest) { + val numRemoved = removeContactRequests(oldRequest.contactId) + if (contactsCount >= numRemoved) contactsCount -= numRemoved + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/group/GroupInvitation.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/group/GroupInvitation.kt new file mode 100644 index 0000000000000000000000000000000000000000..147531a446a0449cfcc8cd17f9c542d06ff8d28b --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/group/GroupInvitation.kt @@ -0,0 +1,18 @@ +package io.xxlabs.messenger.requests.data.group + +import io.xxlabs.messenger.data.datatype.RequestStatus +import io.xxlabs.messenger.data.room.model.Group +import io.xxlabs.messenger.requests.model.GroupInvitation + +/** + * Wrapper class for presenting a [Group] as an invitation. + */ +data class GroupInvitationData( + override val model: Group, + override val unread: Boolean = false, +) : GroupInvitation, Group by model { + override val requestId: ByteArray = model.groupId + override val name: String = model.name + override val requestStatus: RequestStatus = RequestStatus.from(model.status) + override val createdAt: Long = System.currentTimeMillis() +} diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/group/GroupRequestRepository.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/group/GroupRequestRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..f4b84eae67c9803d0d62b76d4fee232ef503ab98 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/group/GroupRequestRepository.kt @@ -0,0 +1,99 @@ +package io.xxlabs.messenger.requests.data.group + +import io.xxlabs.messenger.data.datatype.RequestStatus +import io.xxlabs.messenger.data.room.model.RequestData +import io.xxlabs.messenger.repository.DaoRepository +import io.xxlabs.messenger.requests.bindings.BindingsInvitationsMediator +import io.xxlabs.messenger.requests.data.LocalRequestsDataSource +import io.xxlabs.messenger.requests.data.RequestDataSource +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.support.util.value +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +class GroupRequestsRepository @Inject constructor( + private val daoRepository: DaoRepository, + private val localDataSource: LocalRequestsDataSource, + private val invitationsService: BindingsInvitationsMediator +) : RequestDataSource<GroupInvitation> { + + private val scope = CoroutineScope( + CoroutineName("GroupInvitationsRepo") + + Job() + + Dispatchers.IO + ) + + override val unreadCount: Flow<Int> = localDataSource.unreadCount + + override suspend fun getRequests(): Flow<List<GroupInvitation>> = + localDataSource.getGroupInvitations().map { requestDataList -> + requestDataList.map { requestData -> + val groupData = daoRepository + .getGroup(requestData.requestId) + .value() + GroupInvitationData(groupData, requestData.unread) + } + } + + override fun save(request: GroupInvitation) { + localDataSource.addRequest(RequestData.from(request)) + } + + override fun markAsSeen(request: GroupInvitation) { + scope.launch { + localDataSource.getRequest(request.requestId) + ?.copy(unread = false) + ?.run { + localDataSource.updateRequest(this) + } + } + } + + private fun update(request: GroupInvitation, status: RequestStatus) { + scope.launch { + daoRepository.updateGroupState(request.model.groupId, status).value() + } + } + + override suspend fun accept(request: GroupInvitation): Boolean = withContext(Dispatchers.IO) { + if (daoRepository.acceptGroup(request.model).value() > 0) { + delete(request) + true + } else { + update(request, RequestStatus.CONFIRM_FAIL) + false + } + } + + override fun reject(request: GroupInvitation) { + update(request, RequestStatus.HIDDEN) + } + + override fun delete(request: GroupInvitation) { + scope.launch { + localDataSource.getRequest(request.requestId)?.apply { + localDataSource.deleteRequest(this) + } + } + } + + override fun send(request: GroupInvitation) { + scope.launch { + if (request.requestStatus == RequestStatus.CONFIRM_FAIL) accept(request) + } + } + + override fun resetResentRequests() { + scope.launch { + getRequests().take(2).collectLatest { invitationsList -> + val resentList = invitationsList.filter { + it.requestStatus == RequestStatus.RESENT + } + for (resent in resentList) { + update(resent, RequestStatus.SENT) + } + } + } + } +} diff --git a/app/src/main/java/io/xxlabs/messenger/requests/data/group/InvitationMigrator.kt b/app/src/main/java/io/xxlabs/messenger/requests/data/group/InvitationMigrator.kt new file mode 100644 index 0000000000000000000000000000000000000000..9286d70ef6d5d360b0020b16a333765981f11186 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/data/group/InvitationMigrator.kt @@ -0,0 +1,25 @@ +package io.xxlabs.messenger.requests.data.group + +import io.xxlabs.messenger.repository.DaoRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +object InvitationMigrator { + + /** + * Creates and stores [GroupInvitationData] from existing groups that haven't + * been accepted yet. + */ + suspend fun performMigration( + invitationsDataSource: GroupRequestsRepository, + daoRepository: DaoRepository + ) = withContext(Dispatchers.IO) { + val groupsToMigrate = daoRepository.getAllGroupRequests() + for (group in groupsToMigrate) { + launch { + invitationsDataSource.save(GroupInvitationData(group, false)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestGenericFragment.kt b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestGenericFragment.kt similarity index 96% rename from app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestGenericFragment.kt rename to app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestGenericFragment.kt index 74c27009943e086dbe54b21f966d4d5aee11cbf2..cb0040c3aa78547272d6377c4308c4e525eab825 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestGenericFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestGenericFragment.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.ui.main.requests +package io.xxlabs.messenger.requests.deprecated import android.os.Bundle import android.view.LayoutInflater @@ -53,7 +53,7 @@ class RequestGenericFragment : BaseFragment() { when (RequestStatus.from(contact.status)) { RequestStatus.SEND_FAIL, RequestStatus.SENT -> { Timber.v("Resending request auth channel...") - contactsViewModel.updateAndRequestAuthChannel(contact.marshaled!!) + contactsViewModel.updateAndRequestAuthChannel(contact) (requireActivity() as MainActivity).createSnackMessage( "Sending a new request", true @@ -77,7 +77,7 @@ class RequestGenericFragment : BaseFragment() { override fun onClickUsername(v: View, contact: ContactData) { val bundle = bundleOf("contact_id" to contact.userId) - if (contact.status == RequestStatus.RECEIVED.value) { + if (contact.status == RequestStatus.VERIFIED.value) { navController.navigateSafe(R.id.action_global_contact_invitation, bundle) } else { navController.navigateSafe(R.id.action_global_contact_details, bundle) @@ -144,7 +144,6 @@ class RequestGenericFragment : BaseFragment() { } fun initComponents(root: View) { - contactsViewModel.viewAllRequests() requestsAddContactBtn.setOnSingleClickListener { navController.navigateSafe(R.id.action_requests_to_ud_search) } @@ -229,7 +228,6 @@ class RequestGenericFragment : BaseFragment() { is DataRequestState.Success -> { completeInvitation(true) - contactsViewModel.viewSingleRequest() (requireActivity() as MainActivity).createSnackMessage("Group Confirmed!") } else -> { @@ -276,7 +274,7 @@ class RequestGenericFragment : BaseFragment() { } private fun rejectContact(contact: ContactData) { - contactsViewModel.rejectContact(contact.userId) + contactsViewModel.rejectContact(contact) } private fun acceptGroup(group: GroupData) { @@ -332,9 +330,9 @@ class RequestGenericFragment : BaseFragment() { addSource(contactsViewModel.contactsData) { contacts -> contactRequests = contacts.filter { - it.status == RequestStatus.RECEIVED.value + it.status == RequestStatus.VERIFIED.value || it.status == RequestStatus.VERIFYING.value - || it.status == RequestStatus.UNVERIFIED.value + || it.status == RequestStatus.VERIFICATION_FAIL.value } receivedList = contactRequests + groupInvites value = receivedList diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsAdapter.kt b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsAdapter.kt similarity index 98% rename from app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsAdapter.kt rename to app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsAdapter.kt index 01279daacc29ddf6e4a606cd6e263bed1b3040a4..79951bdad3542bf376eeaf6220dc0f8a5650ed83 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsAdapter.kt +++ b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsAdapter.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.ui.main.requests +package io.xxlabs.messenger.requests.deprecated import android.graphics.BitmapFactory import android.view.LayoutInflater @@ -52,7 +52,7 @@ class RequestsAdapter(private val requestsListener: RequestsListener) : holder.contactPhotoBg.contentDescription = "requests.item.$position.photo.bg" holder.contactPhoto.contentDescription = "requests.item.$position.photo" holder.contactPhotoText.contentDescription = "requests.item.$position.photo.text" - holder.resend.contentDescription = "requests.item.$position.btn.resend" + holder.resend.contentDescription = "requests.item.$position.btn.send" holder.acceptBtn.contentDescription = "requests.item.$position.btn.accept" holder.rejectBtn.contentDescription = "requests.item.$position.btn.reject" } @@ -116,8 +116,8 @@ class RequestsAdapter(private val requestsListener: RequestsListener) : ) { when(RequestStatus.from(item.status)) { RequestStatus.CONFIRM_FAIL, RequestStatus.SEND_FAIL -> updateUiForFailure(holder, item) - RequestStatus.RECEIVED -> updateUiForReceived(holder, item) - RequestStatus.UNVERIFIED -> updateUiForUnverified(holder, item) + RequestStatus.VERIFIED -> updateUiForReceived(holder, item) + RequestStatus.VERIFICATION_FAIL -> updateUiForUnverified(holder, item) RequestStatus.VERIFYING -> updateUiForVerifying(holder, item) RequestStatus.SENT, RequestStatus.RESET_SENT, RequestStatus.RESET_FAIL -> { updateUiForSent(holder, item) diff --git a/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsFilter.kt b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..2e3296a1928ac28b034325b2749b53ede9facae3 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsFilter.kt @@ -0,0 +1,7 @@ +package io.xxlabs.messenger.requests.deprecated + +enum class RequestsFilter { + RECEIVED, + SENT, + FAILED; +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsFragment.kt b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsFragment.kt similarity index 98% rename from app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsFragment.kt rename to app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsFragment.kt index 399316b8afb90f279bd290dbce48ee80016b11f3..0bba4de6aee8176f665e9f41e362036231cdbea1 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsFragment.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.ui.main.requests +package io.xxlabs.messenger.requests.deprecated import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsListener.kt b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsListener.kt similarity index 92% rename from app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsListener.kt rename to app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsListener.kt index 1cdfbc7a8f86ac7a1c42c31773553478598fb3a9..986a78e6dff1bdcd8a8aaa9c113f22f6410a7bbc 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsListener.kt +++ b/app/src/main/java/io/xxlabs/messenger/requests/deprecated/RequestsListener.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.ui.main.requests +package io.xxlabs.messenger.requests.deprecated import android.view.View import io.xxlabs.messenger.data.room.model.ContactData diff --git a/app/src/main/java/io/xxlabs/messenger/requests/model/Request.kt b/app/src/main/java/io/xxlabs/messenger/requests/model/Request.kt new file mode 100644 index 0000000000000000000000000000000000000000..bf6acccdcd72c9ebb76b6bd82c6e7755c02c20d4 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/model/Request.kt @@ -0,0 +1,87 @@ +package io.xxlabs.messenger.requests.model + +import io.xxlabs.messenger.data.datatype.RequestStatus +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.data.room.model.Group +import java.io.Serializable + +sealed interface Request : Serializable { + val requestId: ByteArray + val name: String + val createdAt: Long + val requestStatus: RequestStatus + val unread: Boolean + + override fun equals(other: Any?): Boolean +} + +interface ContactRequest : Request { + val model: Contact +} + +interface GroupInvitation : Request { + val model: Group +} + +class NullRequest : Request { + override val requestId: ByteArray get() = byteArrayOf() + override val name: String get() = "" + override val createdAt: Long get() = 0 + override val requestStatus: RequestStatus get() = RequestStatus.VERIFIED + override val unread: Boolean get() = false + override fun equals(other: Any?): Boolean = other is NullRequest +} + +private data class DummyRequest(val position: Int) : Request { + override val requestId: ByteArray = position.toString().toByteArray() + override val name: String = "Request #$position" + override val createdAt: Long = System.currentTimeMillis() + override val requestStatus: RequestStatus = RequestStatus.SENT + override val unread: Boolean = true +} + +data class DummyContactRequest( + val position: Int, + val request: Request = DummyRequest(position) +) : ContactRequest, Request by request { + override val model: Contact = DummyContact(position) + override val requestStatus: RequestStatus = RequestStatus.from(position % 8) +} + +data class DummyContact( + val position: Int +) : Contact { + override val id: Long = position.toLong() + override val userId: ByteArray = position.toString().toByteArray() + override val username: String = "User $position" + override val status: Int = RequestStatus.SENT.value + override val nickname: String = username + override val photo: ByteArray? = null + override val email: String = "user${position}@gmail.com" + override val phone: String = "+0 123-456-7890" + override val marshaled: ByteArray? = null + override val createdAt: Long = System.currentTimeMillis() + override val displayName: String = username + override val initials: String = "KB" + + override fun hasFacts(): Boolean = false +} + +data class DummyGroupRequest( + val position: Int, + val request: Request = DummyRequest(position + 100) +) : GroupInvitation, Request by request { + override val model: Group = DummyGroup(position + 100) + override val requestStatus: RequestStatus = RequestStatus.SENT +} + +data class DummyGroup( + val position: Int +) : Group { + override val id: Long = position.toLong() + override val groupId: ByteArray = position.toString().toByteArray() + override val name: String = "Group $position" + override val leader: ByteArray = byteArrayOf() + override val serial: ByteArray = byteArrayOf() + override val status: Int = RequestStatus.VERIFIED.value +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsFragment.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..ba86387707e30bd97e67721575be1aeff992228d --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsFragment.kt @@ -0,0 +1,239 @@ +package io.xxlabs.messenger.requests.ui + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.tabs.TabLayoutMediator +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.datatype.RequestStatus +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.data.room.model.ContactData +import io.xxlabs.messenger.data.room.model.Group +import io.xxlabs.messenger.data.room.model.GroupData +import io.xxlabs.messenger.requests.model.ContactRequest +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.requests.ui.accepted.contact.RequestAcceptedDialog +import io.xxlabs.messenger.requests.ui.accepted.group.InvitationAcceptedDialog +import io.xxlabs.messenger.requests.ui.details.contact.RequestDetailsDialog +import io.xxlabs.messenger.requests.ui.details.group.InvitationDetailsDialog +import io.xxlabs.messenger.requests.ui.list.FailedRequestsFragment +import io.xxlabs.messenger.requests.ui.list.ReceivedRequestsFragment +import io.xxlabs.messenger.requests.ui.list.SentRequestsFragment +import io.xxlabs.messenger.support.extensions.setInsets +import io.xxlabs.messenger.support.extensions.toBase64String +import io.xxlabs.messenger.support.toast.CustomToastActivity +import io.xxlabs.messenger.support.toast.ToastUI +import io.xxlabs.messenger.ui.base.BaseFragment +import io.xxlabs.messenger.ui.base.ViewPagerFragmentStateAdapter +import kotlinx.android.synthetic.main.component_toolbar_generic.* +import kotlinx.android.synthetic.main.fragment_requests.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.lang.Exception +import javax.inject.Inject + +class RequestsFragment : BaseFragment() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private val requestsViewModel: RequestsViewModel by viewModels { viewModelFactory } + + private lateinit var navController: NavController + private lateinit var stateAdapter: ViewPagerFragmentStateAdapter + + private lateinit var toastHandler : CustomToastActivity + + override fun onAttach(context: Context) { + super.onAttach(context) + (context as? CustomToastActivity)?.run { + toastHandler = this + } ?: throw Exception("Activity must implement CustomToastActivity!") + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + observeUI() + } + } + return inflater.inflate(R.layout.fragment_requests, container, false) + } + + private fun observeUI() { + requestsViewModel.showReceivedRequestDetails.onEach { request -> + request?.let { + showRequestDetails(request) + requestsViewModel.onRequestDialogShown() + } + }.launchIn(lifecycleScope) + + requestsViewModel.showConnectionAccepted.onEach { request -> + request?.let { + showAccepted(request) + requestsViewModel.onNewConnectionShown() + } + }.launchIn(lifecycleScope) + + requestsViewModel.navigateToMessages.onEach { contact -> + contact?.let { + navigateToChat(contact) + requestsViewModel.onNavigateToMessagesHandled() + } + }.launchIn(lifecycleScope) + + requestsViewModel.showInvitationDetails.onEach { invitation -> + invitation?.let { + showInvitationDetails(invitation) + requestsViewModel.onInvitationDialogShown() + } + }.launchIn(lifecycleScope) + + requestsViewModel.showGroupAccepted.onEach { group -> + group?.let { + showJoined(group) + requestsViewModel.onGroupAcceptedShown() + } + }.launchIn(lifecycleScope) + + + requestsViewModel.navigateToGroupChat.onEach { group -> + group?.let { + navigateToGroup(group) + requestsViewModel.onNavigateToGroupHandled() + } + }.launchIn(lifecycleScope) + + requestsViewModel.customToast.onEach { ui -> + ui?.let { + showCustomToast(ui) + requestsViewModel.onShowToastHandled() + } + }.launchIn(lifecycleScope) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + navController = findNavController() + + initComponents(view) + } + + fun initComponents(root: View) { + toolbarGeneric.setInsets(topMask = WindowInsetsCompat.Type.systemBars()) + toolbarGenericTitle.text = "Requests" + bindListeners() + + root.apply { + setupViewPager(requestsViewPager) + TabLayoutMediator(requestsAppBarTabs, requestsViewPager) { tab, position -> + tab.apply { + text = stateAdapter.getPageTitle(position) + icon = stateAdapter.getIcon(position) + contentDescription = when (position) { + 0 -> "requests.tab.received" + 1 -> "requests.tab.sent" + else -> "requests.tab.failed" + } + } + }.attach() + } + } + + private fun bindListeners() { + toolbarGenericBackBtn.setOnClickListener { + navController.navigateUp() + } + } + + private fun setupViewPager(viewPager: ViewPager2) { + stateAdapter = ViewPagerFragmentStateAdapter(childFragmentManager, lifecycle) + stateAdapter.addFragment( + ReceivedRequestsFragment(), + "Received", + getTabIcon(R.drawable.ic_mail_received) + ) + stateAdapter.addFragment( + SentRequestsFragment(), + "Sent", + getTabIcon(R.drawable.ic_mail_sent) + ) + stateAdapter.addFragment( + FailedRequestsFragment(), + "Failed", + getTabIcon(R.drawable.ic_danger) + ) + + viewPager.adapter = stateAdapter + viewPager.offscreenPageLimit = 3 + + val selectedTab = arguments?.getInt("selectedTab") ?: 0 + viewPager.setCurrentItem(selectedTab, false) + } + + private fun getTabIcon(resourceId: Int): Drawable? { + return try { + ResourcesCompat.getDrawable(resources, resourceId, null) + } catch (e: Exception) { + null + } + } + + private fun showRequestDetails(request: ContactRequest) { + RequestDetailsDialog + .newInstance(request) + .show(childFragmentManager, null) + } + + private fun showInvitationDetails(invitation: GroupInvitation) { + InvitationDetailsDialog + .newInstance(invitation) + .show(childFragmentManager, null) + } + + private fun showAccepted(contact: Contact) { + RequestAcceptedDialog + .newInstance(contact) + .show(childFragmentManager, null) + } + + private fun navigateToChat(contact: Contact) { + val privateMessages = RequestsFragmentDirections.actionGlobalChat() + privateMessages.contactId = contact.userId.toBase64String() + privateMessages.contact = (contact as ContactData).copy(status = RequestStatus.ACCEPTED.value) + findNavController().navigate(privateMessages) + } + + private fun showJoined(group: Group) { + InvitationAcceptedDialog + .newInstance(group) + .show(childFragmentManager, null) + } + + private fun showCustomToast(ui: ToastUI) { + toastHandler.showCustomToast(ui) + } + + private fun navigateToGroup(group: Group) { + val groupMessages = RequestsFragmentDirections.actionGlobalGroupsChat() + groupMessages.groupId = group.groupId.toBase64String() + groupMessages.group = (group as GroupData).copy(status = RequestStatus.ACCEPTED.value) + findNavController().navigate(groupMessages) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsViewModel.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..a802e4178a1e1ce036206980561cc1e0fdb60866 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsViewModel.kt @@ -0,0 +1,535 @@ +package io.xxlabs.messenger.requests.ui + +import android.graphics.Bitmap +import androidx.annotation.StringRes +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow +import androidx.lifecycle.viewModelScope +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.datatype.RequestStatus +import io.xxlabs.messenger.data.datatype.RequestStatus.* +import io.xxlabs.messenger.data.room.model.* +import io.xxlabs.messenger.repository.DaoRepository +import io.xxlabs.messenger.repository.PreferencesRepository +import io.xxlabs.messenger.repository.base.BaseRepository +import io.xxlabs.messenger.requests.data.contact.ContactRequestsRepository +import io.xxlabs.messenger.requests.data.group.GroupRequestsRepository +import io.xxlabs.messenger.requests.model.ContactRequest +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.requests.ui.accepted.contact.RequestAccepted +import io.xxlabs.messenger.requests.ui.accepted.contact.RequestAcceptedListener +import io.xxlabs.messenger.requests.ui.accepted.RequestAcceptedUI +import io.xxlabs.messenger.requests.ui.accepted.group.InvitationAccepted +import io.xxlabs.messenger.requests.ui.accepted.group.InvitationAcceptedListener +import io.xxlabs.messenger.requests.ui.details.contact.RequestDetails +import io.xxlabs.messenger.requests.ui.details.contact.RequestDetailsListener +import io.xxlabs.messenger.requests.ui.details.contact.RequestDetailsUI +import io.xxlabs.messenger.requests.ui.details.group.InvitationDetails +import io.xxlabs.messenger.requests.ui.details.group.InvitationDetailsListener +import io.xxlabs.messenger.requests.ui.details.group.InvitationDetailsUI +import io.xxlabs.messenger.requests.ui.details.group.adapter.MemberItem +import io.xxlabs.messenger.requests.ui.list.adapter.* +import io.xxlabs.messenger.requests.ui.nickname.SaveNickname +import io.xxlabs.messenger.requests.ui.nickname.SaveNicknameListener +import io.xxlabs.messenger.requests.ui.nickname.SaveNicknameUI +import io.xxlabs.messenger.requests.ui.send.* +import io.xxlabs.messenger.support.appContext +import io.xxlabs.messenger.support.toast.ToastUI +import io.xxlabs.messenger.support.util.value +import io.xxlabs.messenger.support.view.BitmapResolver +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class RequestsViewModel @Inject constructor( + private val userDataSource: BaseRepository, + private val daoRepository: DaoRepository, + private val requestsDataSource: ContactRequestsRepository, + private val invitationsDataSource: GroupRequestsRepository, + private val preferences: PreferencesRepository +) : ViewModel(), + RequestItemListener, + RequestDetailsListener, + RequestAcceptedListener, + InvitationDetailsListener, + InvitationAcceptedListener, + SendRequestListener, + SaveNicknameListener +{ + + private val groupInviteCache: MutableMap<ByteArray, GroupInviteItem> = mutableMapOf() + private val contactsCache = MutableStateFlow<List<ContactData>>(listOf()) + private val hiddenRequests = MutableStateFlow<List<RequestItem>>(listOf()) + private var hiddenRequestJob: Job? = null + private val isLoadingGroupMembers = MutableLiveData(true) + + val showReceivedRequestDetails: StateFlow<ContactRequest?> by ::_showReceivedRequestDetails + private val _showReceivedRequestDetails = MutableStateFlow<ContactRequest?>(null) + + val showConnectionAccepted: StateFlow<Contact?> by ::_showConnectionAccepted + private val _showConnectionAccepted = MutableStateFlow<Contact?>(null) + + val navigateToMessages: StateFlow<Contact?> by ::_navigateToMessages + private val _navigateToMessages = MutableStateFlow<Contact?>(null) + + val showInvitationDetails: StateFlow<GroupInvitation?> by ::_showInvitationDetails + private val _showInvitationDetails = MutableStateFlow<GroupInvitation?>(null) + + val showGroupAccepted: StateFlow<Group?> by ::_showGroupAccepted + private val _showGroupAccepted = MutableStateFlow<Group?>(null) + + val navigateToGroupChat: StateFlow<Group?> by ::_navigateToGroupChat + private val _navigateToGroupChat = MutableStateFlow<Group?>(null) + + // TODO: Implement send request in this ViewModel. + val sendContactRequest: StateFlow<ContactData?> by ::_sendContactRequest + private val _sendContactRequest = MutableStateFlow<ContactData?>(null) + + val showCreateNickname: StateFlow<OutgoingRequest?> by ::_showCreateNickname + private val _showCreateNickname = MutableStateFlow<OutgoingRequest?>(null) + + val customToast: Flow<ToastUI?> by ::_customToast + private val _customToast = MutableStateFlow<ToastUI?>(null) + + init { + viewModelScope.launch { cacheContactsList() } + requestsDataSource.resetResentRequests() + invitationsDataSource.resetResentRequests() + } + + private suspend fun cacheContactsList() { + daoRepository.getAllAcceptedContactsLive().asFlow().collect { + contactsCache.value = it + } + } + + private suspend fun getAllRequests() = + getContactRequests().combine(getGroupInvites()){ requests, invites -> + requests + invites + } + + private suspend fun getContactRequests() = + requestsDataSource.getRequests().map { requestsList -> + requestsList.map { request -> + ContactRequestItem( + request, + resolveBitmap(request.model.photo) + ) + } + }.flowOn(Dispatchers.IO) + + private suspend fun resolveBitmap(data: ByteArray?): Bitmap? = withContext(Dispatchers.IO) { + BitmapResolver.getBitmap(data) + } + + private suspend fun getGroupInvites() = + invitationsDataSource.getRequests().map { invitationsList -> + invitationsList.map { invitation -> + GroupInviteItem( + invitation, +// fetchMembers(invitation), + getUsername(invitation.model.leader), + ).also { + groupInviteCache[invitation.requestId] = it + } + } + }.stateIn(viewModelScope) + + suspend fun fetchMembers(invitation: GroupInvitation): Flow<List<MemberItem>> { + isLoadingGroupMembers.value = true + val members = daoRepository.getAllMembers(invitation.model.groupId).value() + val fetchedProfiles = fetchProfiles(members) + return flowOf(getMemberItems(fetchedProfiles, invitation.model)) + } + + private suspend fun fetchProfiles( + members: List<GroupMember> + ): List<ContactData> = suspendCoroutine { continuation -> + val userIds = members.map { it.userId } + userDataSource.getMembersUsername(userIds) { profiles, _, error -> + if (error.isNullOrBlank()) { + profiles?.map { user -> + ContactData.from(user, VERIFYING) + }?.run { + continuation.resume(this) + } ?: continuation.resume(listOf()) + } else { + continuation.resume(listOf()) + } + } + } + + private suspend fun getMemberItems( + profiles: List<ContactData>, + group: Group + ): List<MemberItem> = profiles.map { + getContactOrNull(it.userId)?.let { contact -> + memberFromContact(contact, group) + } ?: memberFromContact(it, group) + }.sortedByDescending { it.isCreator } + .apply { isLoadingGroupMembers.value = false } + + private suspend fun getContactOrNull( + userId: ByteArray + ): ContactData? = withContext(Dispatchers.Default) { + contactsCache.value.firstOrNull { + it.userId.contentEquals(userId) + } + } + + private suspend fun memberFromContact( + contact: Contact, + group: Group + ) = MemberItem.from(contact, group, resolveBitmap(contact.photo)) + + private suspend fun getUsername(userId: ByteArray): String = + getUsernameFromCache(userId) ?: getUsernameFromUd(userId) + + private suspend fun getUsernameFromCache(userId: ByteArray): String? = + withContext(Dispatchers.Default) { + getContactOrNull(userId)?.displayName + } + + private suspend fun getUsernameFromUd(userId: ByteArray): String { + return try { + getUser(userId)?.displayName ?: "xxm User" + } catch (e: Exception) { + delay(1000) + getUsernameFromUd(userId) + } + } + + private suspend fun getUser(userId: ByteArray): ContactData? = + suspendCoroutine { continuation -> + userDataSource.userLookup(userId) { contact, error -> + val contactData = contact?.let { ContactData.from(it) } + if (error.isNullOrBlank()) continuation.resume(contactData) + else continuation.resumeWithException(Exception(error)) + } + } + + suspend fun getFailedRequests() : Flow<List<RequestItem>> = + getAllRequests().map { requests -> + requests + .filter { it.isFailed() } + .ifEmpty { showPlaceholder(R.string.requests_empty_placeholder_failed) } + } + + private fun RequestItem.isFailed(): Boolean { + return when (request.requestStatus) { + RESET_FAIL, SEND_FAIL -> true + else -> false + } + } + + suspend fun getSentRequests() : Flow<List<RequestItem>> = + getAllRequests().map { requests -> + requests + .filter { it.isOutgoing() } + .ifEmpty { showPlaceholder(R.string.requests_empty_placeholder_sent) } + } + + private fun RequestItem.isOutgoing(): Boolean { + return when (request.requestStatus) { + SENT, RESET_SENT, SENDING, RESENT -> true + else -> false + } + } + + suspend fun getReceivedRequests() : Flow<List<RequestItem>> = + getShownRequests().combine(hiddenRequests) { shown, hidden -> + shown + hidden + } + + private suspend fun getShownRequests(): Flow<List<RequestItem>> = + getAllRequests().map { requests -> + requests + .filter { it.isIncoming() } + .ifEmpty { showPlaceholder(R.string.requests_empty_placeholder_received) } + .plus(HiddenRequestToggleItem()) + } + + private fun RequestItem.isIncoming(): Boolean { + return when (request.requestStatus) { + VERIFYING, VERIFIED, VERIFICATION_FAIL -> true + else -> false + } + } + + override fun onShowHiddenToggled(enabled: Boolean) { + if (!enabled) { + hiddenRequestJob?.cancel() + hiddenRequests.value = listOf() + return + } + + hiddenRequestJob = viewModelScope.launch { + getAllRequests().cancellable().collect { requests -> + val hiddenList = requests + .filter { it.isHidden() } + .ifEmpty { showPlaceholder(R.string.requests_empty_placeholder_hidden) } + hiddenRequests.value = hiddenList + } + } + } + + private fun showPlaceholder(@StringRes text: Int): List<RequestItem> = + listOf(EmptyPlaceholderItem(text = appContext().getString(text))) + + private fun RequestItem.isHidden(): Boolean { + return when (request.requestStatus) { + HIDDEN -> true + else -> false + } + } + + override fun onItemClicked(request: RequestItem) { + when (request.request.requestStatus) { + VERIFIED, HIDDEN -> showDetails(request) + } + } + + private fun showDetails(request: RequestItem) { + when (request) { + is ContactRequestItem -> showRequestDialog(request.contactRequest) + is GroupInviteItem -> showInvitationDialog(request.invite) + } + } + + override fun onActionClicked(request: RequestItem) { + when (request.request.requestStatus) { + SEND_FAIL, SENT -> resendRequest(request) + VERIFICATION_FAIL -> retryVerification(request) + } + } + + override fun markAsSeen(request: RequestItem) { + if (request.request.unread) { + when (request) { + is ContactRequestItem -> requestsDataSource.markAsSeen(request.contactRequest) + is GroupInviteItem -> invitationsDataSource.markAsSeen(request.invite) + } + } + } + + private fun showRequestDialog(request: ContactRequest) { + _showReceivedRequestDetails.value = request + } + + fun onRequestDialogShown() { + _showReceivedRequestDetails.value = null + } + + fun onNewConnectionShown() { + _showConnectionAccepted.value = null + } + + private fun retryVerification(request: RequestItem) { + (request as? ContactRequest)?.let { + requestsDataSource.send(request) + } + } + + private fun resendRequest(item: RequestItem) { + when (item) { + is ContactRequestItem -> requestsDataSource.send(item.request as ContactRequest) + is GroupInviteItem -> invitationsDataSource.send(item.request as GroupInvitation) + } + onResend(item) + } + + private fun onResend(item: RequestItem) { + val request = when(item) { + is GroupInviteItem -> "Invitation" + else -> "Request" + } + _customToast.value = ToastUI.create( + body = "$request successfully resent to ${item.request.name}" + ) + } + + suspend fun getRequestDetails(contactRequest: ContactRequest): Flow<RequestDetailsUI?> = + getContactRequests().flatMapLatest { requestsList -> + flow { + requestsList.firstOrNull { request -> + request.id.contentEquals(contactRequest.requestId) + }?.run { + emit(RequestDetails(contactRequest, this@RequestsViewModel)) + } + } + } + + fun getRequestAccepted(contact: Contact): RequestAcceptedUI = + RequestAccepted(contact, this@RequestsViewModel) + + override fun acceptRequest(request: ContactRequest, nickname: String?) { + viewModelScope.launch { + saveContact(request)?.let { contact -> + if (requestsDataSource.accept(request)) { + val updatedContact = updateNickname(request.model, nickname) + showNewConnectionDialog(updatedContact ?: contact) + } + } + } + } + + private suspend fun updateNickname(user: Contact, nickname: String?): ContactData? { + return nickname?.let { nick -> + (user as? ContactData)?.let { contactData -> + val updatedContact = contactData.copy(nickname = nick) + val rowsUpdated = daoRepository + .updateContactNickname(updatedContact) + if (rowsUpdated > 0) updatedContact + else contactData + } + } + } + + private suspend fun saveContact(request: ContactRequest): Contact? { + val rowsUpdated = updateContactStatus(request.model.userId, ACCEPTED) + return if (rowsUpdated > 0) request.model else null + } + + private fun showNewConnectionDialog(contact: Contact) { + _showConnectionAccepted.value = contact + } + + private suspend fun updateContactStatus(userId: ByteArray, status: RequestStatus): Int = + daoRepository.updateContactState(userId, status).value() + + override fun hideRequest(request: ContactRequest) { + viewModelScope.launch { + if (ignoreContact(request) > 0) requestsDataSource.reject(request) + } + } + + private suspend fun ignoreContact(request: ContactRequest): Int = + updateContactStatus(request.model.userId, HIDDEN) + + override fun sendMessage(contact: Contact) { + _navigateToMessages.value = contact + } + + fun onNavigateToMessagesHandled() { + _navigateToMessages.value = null + } + + private fun showInvitationDialog(invite: GroupInvitation) { + _showInvitationDetails.value = invite + } + + fun onInvitationDialogShown() { + _showInvitationDetails.value = null + } + + suspend fun getInvitationDetails(invitation: GroupInvitation): Flow<InvitationDetailsUI?> { + return getCachedDetails(invitation)?.let { flowOf(it) } + ?: getGroupInvites().flatMapLatest { invitesList -> + flow { + invitesList.firstOrNull { invite -> + invite.id.contentEquals(invitation.requestId) + }?.run { + emit(InvitationDetails( + this, + this@RequestsViewModel, + isLoadingGroupMembers) + ) + } + } + } + } + + private fun getCachedDetails(invitation: GroupInvitation): InvitationDetailsUI? = + groupInviteCache[invitation.requestId]?.let { + InvitationDetails(it, this, isLoadingGroupMembers) + } + + fun getInvitationAccepted(group: Group): RequestAcceptedUI = + InvitationAccepted(group, this@RequestsViewModel) + + override fun acceptInvitation(invitation: GroupInvitation) { + viewModelScope.launch { + joinGroup(invitation)?.let { group -> + if (invitationsDataSource.accept(invitation)) showGroupAcceptedDialog(group) + } + } + } + + private suspend fun joinGroup(invitation: GroupInvitation): Group? { + val successful = userDataSource.acceptGroupInvite(invitation.model.serial).value() + return if (successful) invitation.model else null + } + + private fun showGroupAcceptedDialog(group: Group) { + (group as? GroupData)?.copy(status = ACCEPTED.value)?.let { acceptedGroup -> + _showGroupAccepted.value = acceptedGroup + } + } + + fun onGroupAcceptedShown() { + _showGroupAccepted.value = null + } + + override fun hideInvitation(invitation: GroupInvitation) { + invitationsDataSource.reject(invitation) + } + + override fun openGroupChat(group: Group) { + _navigateToGroupChat.value = group + } + + fun onNavigateToGroupHandled() { + _navigateToGroupChat.value = null + } + + fun contactRequestTo(user: Contact): SendRequestUI = + SendRequest( + sender = CurrentUser( + userDataSource.getStoredEmail().ifBlank { null }, + userDataSource.getStoredPhone().ifBlank { null } + ), + receiver = user, + listener = this@RequestsViewModel + ) + + override fun sendRequest(request: OutgoingRequest) { + with (request.sender) { + preferences.shareEmailWhenRequesting = !email.isNullOrBlank() + preferences.sharePhoneWhenRequesting = !phone.isNullOrBlank() + } + + _sendContactRequest.value = request.receiver as? ContactData + _showCreateNickname.value = request + } + + fun onSendRequestHandled() { + _sendContactRequest.value = null + } + + fun getSaveNickname(request: OutgoingRequest): SaveNicknameUI = + SaveNickname(request, this) + + fun onShowCreateNicknameHandled() { + _showCreateNickname.value = null + } + + override fun saveNickname(request: OutgoingRequest, nickname: String?) { + viewModelScope.launch { + updateNickname(request.receiver, nickname) + } + } + + + fun onShowToastHandled() { + _customToast.value = null + } +} + +data class CurrentUser( + override val email: String?, + override val phone: String? +) : RequestSender \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/RequestAcceptedUI.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/RequestAcceptedUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..283ea891ac276477f0cd5605813e7a2681cb2ee8 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/RequestAcceptedUI.kt @@ -0,0 +1,10 @@ +package io.xxlabs.messenger.requests.ui.accepted + +import io.xxlabs.messenger.ui.dialog.components.CloseButtonUI +import io.xxlabs.messenger.ui.dialog.components.PositiveNegativeButtonUI + +interface RequestAcceptedUI : CloseButtonUI, PositiveNegativeButtonUI { + val title: Int + val subtitle: String + val body: Int +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAccepted.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAccepted.kt new file mode 100644 index 0000000000000000000000000000000000000000..63c87b3260e9a05bbbcb9bae3b05be007fd5e4dd --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAccepted.kt @@ -0,0 +1,27 @@ +package io.xxlabs.messenger.requests.ui.accepted.contact + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.requests.ui.accepted.RequestAcceptedUI + +/** + * Presentation logic for an accepted request. + */ +class RequestAccepted( + private val contact: Contact, + private val listener: RequestAcceptedListener +) : RequestAcceptedUI { + override val title: Int = R.string.request_accepted_title + override val subtitle: String = contact.displayName + override val body: Int = R.string.request_accepted_body + + override val positiveLabel: Int = R.string.contact_accepted_positive_button + override val negativeLabel: Int = R.string.contact_accepted_negative_button + override val positiveButtonEnabled: LiveData<Boolean> = MutableLiveData(true) + + override fun onCloseClicked() {} + override fun onPositiveClick() = listener.sendMessage(contact) + override fun onNegativeClick() {} +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAcceptedDialog.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAcceptedDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..20d2dc8d767d68d81e75807f41f2e489e1964683 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAcceptedDialog.kt @@ -0,0 +1,87 @@ +package io.xxlabs.messenger.requests.ui.accepted.contact + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.databinding.ComponentRequestAcceptedDialogBinding +import io.xxlabs.messenger.di.utils.Injectable +import io.xxlabs.messenger.requests.ui.RequestsViewModel +import javax.inject.Inject + +/** + * UI to send a new contact a message or later. + */ +class RequestAcceptedDialog : BottomSheetDialogFragment(), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var binding: ComponentRequestAcceptedDialogBinding + + private val requestsViewModel: RequestsViewModel by viewModels( + ownerProducer = { requireParentFragment() }, + factoryProducer = { viewModelFactory } + ) + private val contact: Contact by lazy { + requireArguments().getSerializable(ARG_CONTACT) as Contact + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ComponentRequestAcceptedDialogBinding.inflate( + inflater, + container, + false + ) + binding.ui = requestsViewModel.getRequestAccepted(contact) + binding.lifecycleOwner = viewLifecycleOwner + initClickListeners() + + return binding.root + } + + private fun initClickListeners() { + binding.closeButtonLayout.closeButton.setOnClickListener { + binding.ui?.onCloseClicked() + dismiss() + } + + binding.dialogButtonLayout.positiveButton.setOnClickListener { + binding.ui?.onPositiveClick() + dismiss() + } + + binding.dialogButtonLayout.negativeButton.setOnClickListener { + binding.ui?.onNegativeClick() + dismiss() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun getTheme(): Int = R.style.RoundedModalBottomSheetDialog + + companion object { + private const val ARG_CONTACT: String = "contact" + + fun newInstance(contact: Contact): RequestAcceptedDialog = + RequestAcceptedDialog().apply { + arguments = Bundle().apply { + putSerializable(ARG_CONTACT, contact) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAcceptedListener.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAcceptedListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..890a728d905d03c262454d98dada81967d3609ba --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/contact/RequestAcceptedListener.kt @@ -0,0 +1,7 @@ +package io.xxlabs.messenger.requests.ui.accepted.contact + +import io.xxlabs.messenger.data.room.model.Contact + +interface RequestAcceptedListener { + fun sendMessage(contact: Contact) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAccepted.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAccepted.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ee7abfd965e158529ed3f00c5030e474c616daf --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAccepted.kt @@ -0,0 +1,27 @@ +package io.xxlabs.messenger.requests.ui.accepted.group + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.room.model.Group +import io.xxlabs.messenger.requests.ui.accepted.RequestAcceptedUI + +/** + * Presentation logic for an accepted group invitation. + */ +class InvitationAccepted( + private val group: Group, + private val listener: InvitationAcceptedListener +) : RequestAcceptedUI { + override val title: Int = R.string.invitation_accepted_title + override val subtitle: String = group.name + override val body: Int = R.string.invitation_accepted_body + + override val positiveLabel: Int = R.string.invitation_accepted_positive_button + override val negativeLabel: Int = R.string.contact_accepted_negative_button + override val positiveButtonEnabled: LiveData<Boolean> = MutableLiveData(true) + + override fun onCloseClicked() {} + override fun onPositiveClick() = listener.openGroupChat(group) + override fun onNegativeClick() {} +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAcceptedDialog.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAcceptedDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..31dbcca8ba3bbf373c122ce36ee52e70c9d9daa5 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAcceptedDialog.kt @@ -0,0 +1,88 @@ +package io.xxlabs.messenger.requests.ui.accepted.group + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.room.model.Group +import io.xxlabs.messenger.databinding.ComponentRequestAcceptedDialogBinding +import io.xxlabs.messenger.di.utils.Injectable +import io.xxlabs.messenger.requests.ui.RequestsViewModel +import io.xxlabs.messenger.requests.ui.details.contact.RequestDetailsDialog +import javax.inject.Inject + +/** + * UI to navigate to group chat immediately or later, after accepting. + */ +class InvitationAcceptedDialog : BottomSheetDialogFragment(), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var binding: ComponentRequestAcceptedDialogBinding + + private val requestsViewModel: RequestsViewModel by viewModels( + ownerProducer = { requireParentFragment() }, + factoryProducer = { viewModelFactory } + ) + private val group: Group by lazy { + requireArguments().getSerializable(ARG_GROUP) as Group + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ComponentRequestAcceptedDialogBinding.inflate( + inflater, + container, + false + ) + binding.ui = requestsViewModel.getInvitationAccepted(group) + binding.lifecycleOwner = viewLifecycleOwner + initClickListeners() + + return binding.root + } + + private fun initClickListeners() { + binding.closeButtonLayout.closeButton.setOnClickListener { + binding.ui?.onCloseClicked() + dismiss() + } + + binding.dialogButtonLayout.positiveButton.setOnClickListener { + binding.ui?.onPositiveClick() + dismiss() + } + + binding.dialogButtonLayout.negativeButton.setOnClickListener { + binding.ui?.onNegativeClick() + dismiss() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun getTheme(): Int = R.style.RoundedModalBottomSheetDialog + + companion object { + private const val ARG_GROUP: String = "group" + + fun newInstance(group: Group): InvitationAcceptedDialog = + InvitationAcceptedDialog().apply { + arguments = Bundle().apply { + putSerializable(ARG_GROUP, group) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAcceptedListener.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAcceptedListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a9b236f90b353c87bdeb6e8902d58355bbc5520 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/accepted/group/InvitationAcceptedListener.kt @@ -0,0 +1,7 @@ +package io.xxlabs.messenger.requests.ui.accepted.group + +import io.xxlabs.messenger.data.room.model.Group + +interface InvitationAcceptedListener { + fun openGroupChat(group: Group) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetails.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetails.kt new file mode 100644 index 0000000000000000000000000000000000000000..984508cebe28e8a6f8eb11187cbba751c08efd8a --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetails.kt @@ -0,0 +1,50 @@ +package io.xxlabs.messenger.requests.ui.details.contact + +import android.text.Editable +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.room.model.formattedEmail +import io.xxlabs.messenger.data.room.model.formattedPhone +import io.xxlabs.messenger.requests.model.ContactRequest + +/** + * [ContactRequest] presentation logic. + */ +class RequestDetails( + private val request: ContactRequest, + private val listener: RequestDetailsListener +) : RequestDetailsUI { + override val username: String = request.model.displayName + override val email: String? = request.model.formattedEmail() + override val phone: String? = request.model.formattedPhone() + + override val nicknameHint: LiveData<String> by ::_nicknameHint + private val _nicknameHint = MutableLiveData(username) + + override val nicknameError: LiveData<String?> by ::_nicknameError + private val _nicknameError = MutableLiveData<String?>(null) + override val maxNicknameLength: Int = 32 + + override val positiveLabel: Int = R.string.request_details_positive_button + override val negativeLabel: Int = R.string.request_details_negative_button + override val positiveButtonEnabled: LiveData<Boolean> = MutableLiveData(true) + + private var nickname: String? = null + + override fun onNicknameInput(editable: Editable) { + nickname = editable.toString() + with (editable) { + _nicknameHint.value = if (isEmpty()) username else "Nickname" + _nicknameError.value = + if (isNotEmpty() && isBlank()) "Cannot be blank." + else null + } + } + + override fun onCloseClicked() {} + + override fun onPositiveClick() = listener.acceptRequest(request, nickname) + + override fun onNegativeClick() = listener.hideRequest(request) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsDialog.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..80b22ea32864bd049acb4012f2aebc71b5059284 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsDialog.kt @@ -0,0 +1,94 @@ +package io.xxlabs.messenger.requests.ui.details.contact + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import io.xxlabs.messenger.R +import io.xxlabs.messenger.databinding.ComponentRequestDetailsDialogBinding +import io.xxlabs.messenger.di.utils.Injectable +import io.xxlabs.messenger.requests.model.ContactRequest +import io.xxlabs.messenger.requests.ui.RequestsViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * UI to accept or hide a [ContactRequest]. + */ +class RequestDetailsDialog : BottomSheetDialogFragment(), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var binding: ComponentRequestDetailsDialogBinding + + private val requestsViewModel: RequestsViewModel by viewModels( + ownerProducer = { requireParentFragment() }, + factoryProducer = { viewModelFactory } + ) + private val request: ContactRequest by lazy { + requireArguments().getSerializable(ARG_REQUEST) as ContactRequest + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ComponentRequestDetailsDialogBinding.inflate( + inflater, + container, + false + ) + + lifecycleScope.launch { + requestsViewModel.getRequestDetails(request).collectLatest { ui -> + ui?.let { binding.ui = it } + } + } + initClickListeners() + binding.lifecycleOwner = viewLifecycleOwner + return binding.root + } + + private fun initClickListeners() { + binding.closeButtonLayout.closeButton.setOnClickListener { + binding.ui?.onCloseClicked() + dismiss() + } + + binding.dialogButtonLayout.positiveButton.setOnClickListener { + binding.ui?.onPositiveClick() + dismiss() + } + + binding.dialogButtonLayout.negativeButton.setOnClickListener { + binding.ui?.onNegativeClick() + dismiss() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun getTheme(): Int = R.style.RoundedModalBottomSheetDialog + + companion object { + private const val ARG_REQUEST: String = "request" + + fun newInstance(request: ContactRequest): RequestDetailsDialog = + RequestDetailsDialog().apply { + arguments = Bundle().apply { + putSerializable(ARG_REQUEST, request) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsListener.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..d0c6da87bda0569dc62f87d941328a9e061c7613 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsListener.kt @@ -0,0 +1,8 @@ +package io.xxlabs.messenger.requests.ui.details.contact + +import io.xxlabs.messenger.requests.model.ContactRequest + +interface RequestDetailsListener { + fun acceptRequest(request: ContactRequest, nickname: String?) + fun hideRequest(request: ContactRequest) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsUI.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..a39570c15c837a7209c63cf2733a68b8dd84cfae --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/contact/RequestDetailsUI.kt @@ -0,0 +1,16 @@ +package io.xxlabs.messenger.requests.ui.details.contact + +import android.text.Editable +import androidx.lifecycle.LiveData +import io.xxlabs.messenger.ui.dialog.components.CloseButtonUI +import io.xxlabs.messenger.ui.dialog.components.PositiveNegativeButtonUI + +interface RequestDetailsUI : CloseButtonUI, PositiveNegativeButtonUI { + val username: String + val email: String? + val phone: String? + val nicknameHint: LiveData<String> + val nicknameError: LiveData<String?> + val maxNicknameLength: Int + fun onNicknameInput(editable: Editable) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetails.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetails.kt new file mode 100644 index 0000000000000000000000000000000000000000..af66c140279cc9e49466c3f1ded4a40f6797312a --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetails.kt @@ -0,0 +1,30 @@ +package io.xxlabs.messenger.requests.ui.details.group + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.xxlabs.messenger.R +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.requests.ui.details.group.adapter.MemberItem +import io.xxlabs.messenger.requests.ui.list.adapter.GroupInviteItem + +/** + * [GroupInvitation] presentation logic. + */ +class InvitationDetails( + private val item: GroupInviteItem, + private val listener: InvitationDetailsListener, + private val _isLoading: LiveData<Boolean> +) : InvitationDetailsUI { + override val groupName: String = item.invite.name + override val isLoading: LiveData<Boolean> by ::_isLoading + + override val positiveLabel: Int = R.string.invitation_details_positive_button + override val negativeLabel: Int = R.string.invitation_details_negative_button + override val positiveButtonEnabled: LiveData<Boolean> = MutableLiveData(true) + + override fun onCloseClicked() {} + + override fun onPositiveClick() = listener.acceptInvitation(item.invite) + + override fun onNegativeClick() = listener.hideInvitation(item.invite) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsDialog.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3e4442c4cfc573586779bfcd4a386aea3c029dc --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsDialog.kt @@ -0,0 +1,125 @@ +package io.xxlabs.messenger.requests.ui.details.group + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import io.xxlabs.messenger.R +import io.xxlabs.messenger.databinding.ComponentInvitationDetailsDialogBinding +import io.xxlabs.messenger.di.utils.Injectable +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.requests.ui.RequestsViewModel +import io.xxlabs.messenger.requests.ui.details.group.adapter.MembersAdapter +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * UI to accept or hide a [GroupInvitation]. + */ +class InvitationDetailsDialog : BottomSheetDialogFragment(), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var binding: ComponentInvitationDetailsDialogBinding + + private val requestsViewModel: RequestsViewModel by viewModels( + ownerProducer = { requireParentFragment() }, + factoryProducer = { viewModelFactory } + ) + private val invitation: GroupInvitation by lazy { + requireArguments().getSerializable(ARG_REQUEST) as GroupInvitation + } + + private val membersAdapter = MembersAdapter() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ComponentInvitationDetailsDialogBinding.inflate( + inflater, + container, + false + ) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + initUI() + } + } + + initClickListeners() + binding.lifecycleOwner = viewLifecycleOwner + return binding.root + } + + private suspend fun initUI() { + requestsViewModel.getInvitationDetails(invitation).onEach { ui -> + ui?.let { + binding.ui = it + initRecyclerView() + } + }.launchIn(lifecycleScope) + + requestsViewModel.fetchMembers(invitation).onEach { members -> + if (members.isNotEmpty()) + membersAdapter.showMembers(members) + }.launchIn(lifecycleScope) + } + + private fun initClickListeners() { + binding.closeButtonLayout.closeButton.setOnClickListener { + binding.ui?.onCloseClicked() + dismiss() + } + + binding.dialogButtonLayout.positiveButton.setOnClickListener { + binding.ui?.onPositiveClick() + dismiss() + } + + binding.dialogButtonLayout.negativeButton.setOnClickListener { + binding.ui?.onNegativeClick() + dismiss() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + private fun initRecyclerView() { + binding.invitationDetailsMembersList.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = membersAdapter + } + } + + override fun getTheme(): Int = R.style.RoundedModalBottomSheetDialog + + companion object { + private const val ARG_REQUEST: String = "invitation" + + fun newInstance(request: GroupInvitation): InvitationDetailsDialog = + InvitationDetailsDialog().apply { + arguments = Bundle().apply { + putSerializable(ARG_REQUEST, request) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsListener.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..f8e86b9ac233d05144734addf1ec7f83d50ddbc8 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsListener.kt @@ -0,0 +1,8 @@ +package io.xxlabs.messenger.requests.ui.details.group + +import io.xxlabs.messenger.requests.model.GroupInvitation + +interface InvitationDetailsListener { + fun acceptInvitation(invitation: GroupInvitation) + fun hideInvitation(invitation: GroupInvitation) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsUI.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..66f381116bb8b6644f1115e24d05ed86468dd88c --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/InvitationDetailsUI.kt @@ -0,0 +1,10 @@ +package io.xxlabs.messenger.requests.ui.details.group + +import androidx.lifecycle.LiveData +import io.xxlabs.messenger.ui.dialog.components.CloseButtonUI +import io.xxlabs.messenger.ui.dialog.components.PositiveNegativeButtonUI + +interface InvitationDetailsUI : CloseButtonUI, PositiveNegativeButtonUI { + val groupName: String + val isLoading: LiveData<Boolean> +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MemberItem.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MemberItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..83d2c2bbc1785ccfc1134f568e1ff7d9791c468a --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MemberItem.kt @@ -0,0 +1,43 @@ +package io.xxlabs.messenger.requests.ui.details.group.adapter + +import android.graphics.Bitmap +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.datatype.RequestStatus +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.data.room.model.Group +import io.xxlabs.messenger.requests.ui.list.adapter.ItemThumbnail +import io.xxlabs.messenger.support.appContext + +data class MemberItem( + override val itemPhoto: Bitmap? = null, + override val itemIconRes: Int? = null, + override val itemInitials: String? = "XX", + val name: String = "xxMessenger User", + val isCreator: Boolean = false, + private val isContact: Boolean = false, +) : ItemThumbnail { + + val description: String? = when { + isCreator -> appContext().getString(R.string.group_member_creator) + !isContact -> appContext().getString(R.string.group_member_not_connection) + else -> null + } + + @ColorRes + val descriptionTextColor: Int = when { + isCreator -> appContext().getColor(R.color.accent_safe) + else -> appContext().getColor(R.color.neutral_secondary) + } + + companion object { + fun from(contact: Contact, group: Group, bitmap: Bitmap?) = MemberItem( + itemPhoto = bitmap, + itemInitials = contact.initials, + name = contact.displayName, + isContact = contact.status == RequestStatus.ACCEPTED.value, + isCreator = group.leader.contentEquals(contact.userId) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MemberViewHolder.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MemberViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..93da1abe3fb7d4bc51b0cfd19bd9a29afbd57591 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MemberViewHolder.kt @@ -0,0 +1,26 @@ +package io.xxlabs.messenger.requests.ui.details.group.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.xxlabs.messenger.databinding.ListItemGroupMemberBinding + +class MemberViewHolder( + private val binding: ListItemGroupMemberBinding +) : RecyclerView.ViewHolder(binding.root) { + + fun onBind(ui: MemberItem) { + binding.ui = ui + } + + companion object { + fun create(parent: ViewGroup): MemberViewHolder { + val binding = ListItemGroupMemberBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return MemberViewHolder(binding) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MembersAdapter.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MembersAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..bf7abb4598243811ee02d025db3126ed843773e7 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/details/group/adapter/MembersAdapter.kt @@ -0,0 +1,28 @@ +package io.xxlabs.messenger.requests.ui.details.group.adapter + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +/** + * Displays members of a group. + */ +class MembersAdapter( +// private val members: List<MemberItem> +) : RecyclerView.Adapter<MemberViewHolder>() { + + private var members = listOf<MemberItem>() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemberViewHolder = + MemberViewHolder.create(parent) + + override fun onBindViewHolder(holder: MemberViewHolder, position: Int) { + holder.onBind(members[position]) + } + + override fun getItemCount(): Int = members.count() + + fun showMembers(membersList: List<MemberItem>) { + members = membersList + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/RequestListFragment.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/RequestListFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..aa431b571be0301b3437458eda2c30e453ac4921 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/RequestListFragment.kt @@ -0,0 +1,93 @@ +package io.xxlabs.messenger.requests.ui.list + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.data.room.model.Group +import io.xxlabs.messenger.databinding.FragmentRequestListBinding +import io.xxlabs.messenger.di.utils.Injectable +import io.xxlabs.messenger.requests.model.ContactRequest +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.requests.ui.RequestsViewModel +import io.xxlabs.messenger.requests.ui.accepted.contact.RequestAcceptedDialog +import io.xxlabs.messenger.requests.ui.accepted.group.InvitationAcceptedDialog +import io.xxlabs.messenger.requests.ui.details.contact.RequestDetailsDialog +import io.xxlabs.messenger.requests.ui.details.group.InvitationDetailsDialog +import io.xxlabs.messenger.requests.ui.list.adapter.RequestItem +import io.xxlabs.messenger.requests.ui.list.adapter.RequestsAdapter +import io.xxlabs.messenger.support.extensions.toBase64String +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +abstract class RequestListFragment : Fragment(), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + protected val requestsViewModel: RequestsViewModel by viewModels( + ownerProducer = { requireParentFragment() }, + factoryProducer = { viewModelFactory } + ) + + private val requestsAdapter: RequestsAdapter by lazy { + RequestsAdapter(requestsViewModel) + } + private lateinit var binding: FragmentRequestListBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentRequestListBinding.inflate(inflater, container, false) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + getRequests().collect { requests -> + requestsAdapter.submitList(requests) + } + } + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initComponents() + } + + private fun initComponents() { + binding.requestsListRV.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = requestsAdapter + } + } + + abstract suspend fun getRequests(): Flow<List<RequestItem>> +} + + +class FailedRequestsFragment : RequestListFragment() { + override suspend fun getRequests(): Flow<List<RequestItem>> = + requestsViewModel.getFailedRequests() +} + +class SentRequestsFragment : RequestListFragment() { + override suspend fun getRequests(): Flow<List<RequestItem>> = + requestsViewModel.getSentRequests() +} + +class ReceivedRequestsFragment : RequestListFragment() { + override suspend fun getRequests(): Flow<List<RequestItem>> = + requestsViewModel.getReceivedRequests() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/ItemThumbnail.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/ItemThumbnail.kt new file mode 100644 index 0000000000000000000000000000000000000000..885ca5c128613bf30f5a2d063b7bc08b217f52dd --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/ItemThumbnail.kt @@ -0,0 +1,9 @@ +package io.xxlabs.messenger.requests.ui.list.adapter + +import android.graphics.Bitmap + +interface ItemThumbnail { + val itemPhoto: Bitmap? + val itemIconRes: Int? + val itemInitials: String? +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItem.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..cee801a26b8ac7020688404802d2642be9c7a9d7 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItem.kt @@ -0,0 +1,110 @@ +package io.xxlabs.messenger.requests.ui.list.adapter + +import android.graphics.Bitmap +import androidx.annotation.IdRes +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.datatype.RequestStatus.* +import io.xxlabs.messenger.data.room.model.formattedEmail +import io.xxlabs.messenger.data.room.model.formattedPhone +import io.xxlabs.messenger.requests.model.ContactRequest +import io.xxlabs.messenger.requests.model.GroupInvitation +import io.xxlabs.messenger.requests.model.NullRequest +import io.xxlabs.messenger.requests.model.Request +import io.xxlabs.messenger.support.appContext + +sealed class RequestItem(val request: Request) : ItemThumbnail { + open val id: ByteArray = request.requestId + open val title: String = request.name + open val timestamp: Long = request.createdAt + + abstract val subtitle: String? + abstract val details: String? + + val actionLabel: String? = + when (request.requestStatus) { + VERIFYING -> appContext().getString(R.string.request_item_action_verifying) + VERIFICATION_FAIL -> appContext().getString(R.string.request_item_action_failed_verification) + SEND_FAIL, SENT -> appContext().getString(R.string.request_item_action_retry) + RESENT -> appContext().getString(R.string.request_item_action_resent) + else -> null + } + + val actionIcon: Int? = + when (request.requestStatus) { + VERIFICATION_FAIL -> R.drawable.ic_info_outline_24dp + SEND_FAIL, SENT -> R.drawable.ic_retry + RESENT -> R.drawable.ic_check_green + else -> null + } + + val actionIconColor: Int? = + when (request.requestStatus) { + VERIFICATION_FAIL -> R.color.accent_danger + SEND_FAIL, SENT-> R.color.brand_default + RESENT -> R.color.accent_success + else -> null + } + + @IdRes + val actionTextStyle: Int? = + when (request.requestStatus) { + VERIFYING -> R.style.request_item_verifying + VERIFICATION_FAIL -> R.style.request_item_error + SEND_FAIL, SENT -> R.style.request_item_retry + RESENT -> R.style.request_item_resent + else -> null + } +} + +data class ContactRequestItem( + val contactRequest: ContactRequest, + val photo: Bitmap?, +) : RequestItem(contactRequest) { + override val subtitle: String? = null + override val details: String? = contactRequest.getContactInfo() + override val itemPhoto: Bitmap? = photo + override val itemInitials: String = contactRequest.model.initials + override val itemIconRes: Int? = null + + private fun ContactRequest.getContactInfo(): String? = + with ("${model.formattedEmail() ?: ""}\n${model.formattedPhone() ?: ""}") { + when { + isNullOrBlank() -> null + else -> trim() + } + } +} + +data class GroupInviteItem( + val invite: GroupInvitation, +// val membersList: List<MemberItem>, + private val groupCreator: String, +) : RequestItem(invite) { + override val subtitle: String = groupCreator // TODO: membersList.first() + override val details: String? = null + override val itemPhoto: Bitmap? = null + override val itemInitials: String? = null + override val itemIconRes: Int = R.drawable.ic_group_chat +} + +data class EmptyPlaceholderItem( + val placeholder: NullRequest = NullRequest(), + val text: String = "" +) : RequestItem(placeholder) { + override val title = text + override val itemPhoto: Bitmap? = null + override val itemIconRes: Int? = null + override val itemInitials: String? = null + override val subtitle: String? = null + override val details: String? = null +} + +data class HiddenRequestToggleItem( + val placeholder: NullRequest = NullRequest(), +) : RequestItem(placeholder) { + override val itemPhoto: Bitmap? = null + override val itemIconRes: Int? = null + override val itemInitials: String? = null + override val subtitle: String? = null + override val details: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemListener.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..848b730ad9387c63114a7ab6a3d67c6dd78fea2b --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemListener.kt @@ -0,0 +1,7 @@ +package io.xxlabs.messenger.requests.ui.list.adapter + +interface RequestItemListener : ShowHiddenUI { + fun onItemClicked(request: RequestItem) + fun onActionClicked(request: RequestItem) + fun markAsSeen(request: RequestItem) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemViewHolder.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..aded82f79dacdcba9ff4328b3abf276efe3e8ba8 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemViewHolder.kt @@ -0,0 +1,101 @@ +package io.xxlabs.messenger.requests.ui.list.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.google.firebase.crashlytics.FirebaseCrashlytics +import io.xxlabs.messenger.R +import io.xxlabs.messenger.databinding.ListItemEmptyPlaceholderBinding +import io.xxlabs.messenger.databinding.ListItemHiddenRequestsToggleBinding +import io.xxlabs.messenger.databinding.ListItemRequestBinding +import timber.log.Timber +import java.io.InvalidObjectException + +abstract class RequestItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { + abstract fun onBind(ui: RequestItem, listener: RequestItemListener) +} + +class RequestViewHolder( + private val binding: ListItemRequestBinding +) : RequestItemViewHolder(binding.root) { + + override fun onBind(ui: RequestItem, listener: RequestItemListener) { + binding.ui = ui + binding.listener = listener + } + + companion object { + fun create(parent: ViewGroup): RequestViewHolder { + val binding = ListItemRequestBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return RequestViewHolder(binding) + } + } +} + +class Placeholder( + private val binding: ListItemEmptyPlaceholderBinding +) : RequestItemViewHolder(binding.root) { + + override fun onBind(ui: RequestItem, listener: RequestItemListener) { + binding.ui = ui + } + + companion object { + fun create(parent: ViewGroup): Placeholder { + val binding = ListItemEmptyPlaceholderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return Placeholder(binding) + } + } +} + +class HiddenRequestToggle( + private val binding: ListItemHiddenRequestsToggleBinding +) : RequestItemViewHolder(binding.root) { + + override fun onBind(ui: RequestItem, listener: RequestItemListener) { + binding.listener = listener + } + + companion object { + fun create(parent: ViewGroup): HiddenRequestToggle { + val binding = ListItemHiddenRequestsToggleBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return HiddenRequestToggle(binding) + } + } +} + +/** + * Displays an invisible ViewHolder and logs the event to prevent + * locking the screen in a permanent crash state. + */ +class InvalidViewType(view: View) : RequestItemViewHolder(view) { + override fun onBind(ui: RequestItem, listener: RequestItemListener) { + FirebaseCrashlytics.getInstance().recordException( + InvalidObjectException("Attempted to show an invalid view type.") + ) + } + + companion object { + fun create(parent: ViewGroup): InvalidViewType { + val view = LayoutInflater.from(parent.context).inflate( + R.layout.list_item_invalid, + parent, + false + ) + return InvalidViewType(view) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestsAdapter.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestsAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..237fbe265f672d0f7544bfb621165ad725a2ab40 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestsAdapter.kt @@ -0,0 +1,65 @@ +package io.xxlabs.messenger.requests.ui.list.adapter + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import io.xxlabs.messenger.requests.ui.list.adapter.RequestsAdapter.ViewType.* + +class RequestsAdapter( + private val listener: RequestItemListener +): ListAdapter<RequestItem, RequestItemViewHolder>(RequestsDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RequestItemViewHolder { + return when (ViewType.from(viewType)) { + REQUEST, INVITE -> RequestViewHolder.create(parent) + PLACEHOLDER -> Placeholder.create(parent) + SWITCH -> HiddenRequestToggle.create(parent) + OTHER -> InvalidViewType.create(parent) + } + } + + override fun onBindViewHolder(holder: RequestItemViewHolder, position: Int) { + with(currentList[position]) { + holder.onBind(this, listener) + listener.markAsSeen(this) + } + } + + override fun getItemViewType(position: Int): Int { + return with (currentList[position]) { + val status = request.requestStatus.value + val model = when (this) { + is ContactRequestItem -> REQUEST.value + is GroupInviteItem -> INVITE.value + is EmptyPlaceholderItem -> PLACEHOLDER.value + is HiddenRequestToggleItem -> SWITCH.value + else -> OTHER.value + } + status + model + } + } + + private enum class ViewType(val value: Int) { + REQUEST(100), + INVITE(200), + PLACEHOLDER(300), + SWITCH(400), + OTHER(500); + + companion object { + fun from(value: Int): ViewType { + return values().firstOrNull { + (value / 100) * 100 == it.value + } ?: OTHER + } + } + } +} + +class RequestsDiffCallback : DiffUtil.ItemCallback<RequestItem>() { + override fun areItemsTheSame(oldItem: RequestItem, newItem: RequestItem): Boolean = + oldItem.id.contentEquals(newItem.id) + + override fun areContentsTheSame(oldItem: RequestItem, newItem: RequestItem): Boolean = + oldItem == newItem +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/ShowHiddenUI.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/ShowHiddenUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..de43957fb2715af2161fdfa43a06673d4de71285 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/ShowHiddenUI.kt @@ -0,0 +1,5 @@ +package io.xxlabs.messenger.requests.ui.list.adapter + +interface ShowHiddenUI { + fun onShowHiddenToggled(enabled: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNickname.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNickname.kt new file mode 100644 index 0000000000000000000000000000000000000000..9e2143fcdf1ef29e5257c0c66635b76c7d753f69 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNickname.kt @@ -0,0 +1,34 @@ +package io.xxlabs.messenger.requests.ui.nickname + +import android.text.Editable +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.xxlabs.messenger.requests.ui.send.OutgoingRequest + +class SaveNickname( + private val outgoingRequest: OutgoingRequest, + private val listener: SaveNicknameListener +): SaveNicknameUI { + override val nicknameHint: LiveData<String> by ::_nicknameHint + private val _nicknameHint = MutableLiveData(outgoingRequest.receiver.displayName) + + override val nicknameError: LiveData<String?> by ::_nicknameError + private val _nicknameError = MutableLiveData<String?>(null) + override val maxNicknameLength: Int = 32 + private var nickname: String? = null + + override val positiveButtonEnabled: LiveData<Boolean> = MutableLiveData(true) + + override fun onNicknameInput(editable: Editable) { + nickname = editable.toString() + with (editable) { + _nicknameHint.value = if (isEmpty()) outgoingRequest.receiver.displayName else "Nickname" + _nicknameError.value = + if (isNotEmpty() && isBlank()) "Cannot be blank." + else null + } + } + override fun onPositiveClick() = listener.saveNickname(outgoingRequest, nickname) + + override fun onCloseClicked() {} +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameDialog.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..ac7945cd120e48756ef1106f9e6ba1c6c3cbca6b --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameDialog.kt @@ -0,0 +1,69 @@ +package io.xxlabs.messenger.requests.ui.nickname + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import io.xxlabs.messenger.databinding.ComponentSendRequestNicknameBinding +import io.xxlabs.messenger.di.utils.Injectable +import io.xxlabs.messenger.requests.ui.RequestsViewModel +import io.xxlabs.messenger.requests.ui.send.OutgoingRequest +import io.xxlabs.messenger.ui.dialog.ExpandedBottomSheetDialog +import javax.inject.Inject + +class SaveNicknameDialog : ExpandedBottomSheetDialog(), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var binding: ComponentSendRequestNicknameBinding + + private val requestsViewModel: RequestsViewModel by viewModels( + ownerProducer = { requireParentFragment() }, + factoryProducer = { viewModelFactory } + ) + private val request: OutgoingRequest by lazy { + requireArguments().getSerializable(ARG_REQUEST) as OutgoingRequest + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ComponentSendRequestNicknameBinding.inflate( + inflater, + container, + false + ) + binding.ui = requestsViewModel.getSaveNickname(request) + binding.lifecycleOwner = viewLifecycleOwner + initClickListeners() + + return binding.root + } + + private fun initClickListeners() { + binding.closeButtonLayout.closeButton.setOnClickListener { + binding.ui?.onCloseClicked() + dismiss() + } + + binding.saveNicknameButton.setOnClickListener { + binding.ui?.onPositiveClick() + dismiss() + } + } + + companion object { + private const val ARG_REQUEST: String = "request" + + fun newInstance(request: OutgoingRequest): SaveNicknameDialog = + SaveNicknameDialog().apply { + arguments = Bundle().apply { + putSerializable(ARG_REQUEST, request) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameListener.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..3cbeae119dec4b2439aa2d27e577ee3ff07038db --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameListener.kt @@ -0,0 +1,7 @@ +package io.xxlabs.messenger.requests.ui.nickname + +import io.xxlabs.messenger.requests.ui.send.OutgoingRequest + +interface SaveNicknameListener { + fun saveNickname(request: OutgoingRequest, nickname: String?) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameUI.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..480af603e1fd7111bb4d6dca8c1784fa9cce51d5 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/nickname/SaveNicknameUI.kt @@ -0,0 +1,14 @@ +package io.xxlabs.messenger.requests.ui.nickname + +import android.text.Editable +import androidx.lifecycle.LiveData +import io.xxlabs.messenger.ui.dialog.components.CloseButtonUI + +interface SaveNicknameUI : CloseButtonUI { + val nicknameHint: LiveData<String> + val nicknameError: LiveData<String?> + val maxNicknameLength: Int + val positiveButtonEnabled: LiveData<Boolean> + fun onNicknameInput(editable: Editable) + fun onPositiveClick() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequest.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..db3c041ccabee88d4ef7819d7adb12e0d80355dc --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequest.kt @@ -0,0 +1,117 @@ +package io.xxlabs.messenger.requests.ui.send + +import android.text.* +import android.text.Annotation +import android.text.style.ForegroundColorSpan +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.xxlabs.messenger.R +import io.xxlabs.messenger.data.data.Country +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.support.appContext +import java.io.Serializable + +interface RequestSender : Serializable { + val email: String? + val phone: String? +} + +class SendRequest( + private val sender: RequestSender, + private val receiver: Contact, + private val listener: SendRequestListener +) : SendRequestUI { + override val body: Spanned = getSpannedBody(receiver.displayName, receiver.receiverFact()) + + override val senderEmail: String = sender.email ?: getPlaceholder() + override val senderPhone: String = formattedPhone() ?: getPlaceholder() + override val emailToggleEnabled: Boolean = !sender.email.isNullOrBlank() + override val phoneToggleEnabled: Boolean = !sender.phone.isNullOrBlank() + + private fun formattedPhone(): String? = sender.phone?.let { Country.toFormattedNumber(it) } + + override val positiveLabel: Int = R.string.send_request_positive_button + override val negativeLabel: Int = R.string.send_request_negative_button + override val positiveButtonEnabled: LiveData<Boolean> = MutableLiveData(true) + + private var includeEmail = false + private var includePhone = false + + private fun getPlaceholder(): String = + appContext().getString(R.string.send_request_fact_placeholder) + + override fun onEmailToggled(enabled: Boolean) { includeEmail = enabled } + + override fun onPhoneToggled(enabled: Boolean) { includePhone = enabled } + + override fun onCloseClicked() {} + + override fun onPositiveClick() { + listener.sendRequest(createOutgoingRequest()) + } + + private fun createOutgoingRequest(): OutgoingRequest { + val requestSender = object : RequestSender { + override val email: String? = if (includeEmail) senderEmail else null + override val phone: String? = if (includePhone) senderPhone else null + } + return object : OutgoingRequest { + override val sender: RequestSender = requestSender + override val receiver: Contact = this@SendRequest.receiver + } + } + + override fun onNegativeClick() {} + + private fun Contact.receiverFact(): String? { + return email.ifBlank { phone.ifBlank { null } } + } + + private fun getSpannedBody(receiverName: String, receiverFact: String? = null): Spanned { + val body = appContext().getText(R.string.send_request_body) as SpannedString + val spannedBody = SpannableStringBuilder(body) + val highlight = appContext().getColor(R.color.brand_default) + return spannedBody.applyFormatString(receiverName, receiverFact, highlight) + } + + private fun SpannableStringBuilder.applyFormatString( + username: String, + userFact: String?, + color: Int + ): Spanned { + val annotations = getSpans(0, this.length, Annotation::class.java) + annotations.forEach { annotation -> + when (annotation.value) { + "receiverName" -> { + replace( + this.getSpanStart(annotation), + this.getSpanEnd(annotation), + "$username " + ) + setSpan( + ForegroundColorSpan(color), + getSpanStart(annotation), + getSpanEnd(annotation), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + "receiverFact" -> { + replace( + this.getSpanStart(annotation), + this.getSpanEnd(annotation), + userFact?.let { "($it) " } ?: "" + ) + userFact?.let { + setSpan( + ForegroundColorSpan(color), + getSpanStart(annotation), + getSpanEnd(annotation), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + } + } + return this + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestDialog.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..f1b800e235d559edaa843d240ee2e9c068f72d8a --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestDialog.kt @@ -0,0 +1,77 @@ +package io.xxlabs.messenger.requests.ui.send + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.databinding.ComponentSendRequestDialogBinding +import io.xxlabs.messenger.di.utils.Injectable +import io.xxlabs.messenger.requests.ui.RequestsViewModel +import io.xxlabs.messenger.ui.dialog.ExpandedBottomSheetDialog +import javax.inject.Inject + +/** + * UI to create a [ContactRequest]. + */ +class SendRequestDialog : ExpandedBottomSheetDialog(), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var binding: ComponentSendRequestDialogBinding + + private val requestsViewModel: RequestsViewModel by viewModels( + ownerProducer = { requireParentFragment() }, + factoryProducer = { viewModelFactory } + ) + private val user: Contact by lazy { + requireArguments().getSerializable(ARG_USER) as Contact + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ComponentSendRequestDialogBinding.inflate( + inflater, + container, + false + ) + binding.ui = requestsViewModel.contactRequestTo(user) + binding.lifecycleOwner = viewLifecycleOwner + initClickListeners() + + return binding.root + } + + private fun initClickListeners() { + binding.closeButtonLayout.closeButton.setOnClickListener { + binding.ui?.onCloseClicked() + dismiss() + } + + binding.dialogButtonLayout.positiveButton.setOnClickListener { + binding.ui?.onPositiveClick() + dismiss() + } + + binding.dialogButtonLayout.negativeButton.setOnClickListener { + binding.ui?.onNegativeClick() + dismiss() + } + } + + companion object { + private const val ARG_USER: String = "user" + + fun newInstance(user: Contact): SendRequestDialog = + SendRequestDialog().apply { + arguments = Bundle().apply { + putSerializable(ARG_USER, user) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestListener.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..956c5fff789ab974e8d1f6e61f5ba1686c4b5861 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestListener.kt @@ -0,0 +1,14 @@ +package io.xxlabs.messenger.requests.ui.send + +import io.xxlabs.messenger.data.room.model.Contact +import java.io.Serializable + + +interface SendRequestListener { + fun sendRequest(request: OutgoingRequest) +} + +interface OutgoingRequest : Serializable{ + val sender: RequestSender + val receiver: Contact +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestUI.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..72065d21cad6a5d1ec1ec1067005ff477fab0d1e --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/send/SendRequestUI.kt @@ -0,0 +1,15 @@ +package io.xxlabs.messenger.requests.ui.send + +import android.text.Spanned +import io.xxlabs.messenger.ui.dialog.components.CloseButtonUI +import io.xxlabs.messenger.ui.dialog.components.PositiveNegativeButtonUI + +interface SendRequestUI : CloseButtonUI, PositiveNegativeButtonUI { + val body: Spanned + val senderEmail: String + val senderPhone: String + val emailToggleEnabled: Boolean + val phoneToggleEnabled: Boolean + fun onEmailToggled(enabled: Boolean) + fun onPhoneToggled(enabled: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/support/dialog/action/ActionDialogUI.kt b/app/src/main/java/io/xxlabs/messenger/support/dialog/action/ActionDialogUI.kt deleted file mode 100644 index 3f1655116bb862f71d192f296e6a9c4c2c2ee0f2..0000000000000000000000000000000000000000 --- a/app/src/main/java/io/xxlabs/messenger/support/dialog/action/ActionDialogUI.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.xxlabs.messenger.support.dialog.action - -import io.xxlabs.messenger.support.dialog.confirm.ConfirmDialogUI - -interface ActionDialogUI : ConfirmDialogUI { - - companion object Factory { - fun create(confirmDialogUI: ConfirmDialogUI) : ActionDialogUI { - return object : ActionDialogUI, ConfirmDialogUI by confirmDialogUI {} - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/support/toast/CustomToastActivity.kt b/app/src/main/java/io/xxlabs/messenger/support/toast/CustomToastActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..159d11e82ce1d10881ad5b3ae6fc9f11228a2fb9 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/support/toast/CustomToastActivity.kt @@ -0,0 +1,5 @@ +package io.xxlabs.messenger.support.toast + +interface CustomToastActivity { + fun showCustomToast(ui: ToastUI) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/support/toast/ToastUI.kt b/app/src/main/java/io/xxlabs/messenger/support/toast/ToastUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..44bd9f779c4d00b08909e711762d0111f0d4bbcf --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/support/toast/ToastUI.kt @@ -0,0 +1,40 @@ +package io.xxlabs.messenger.support.toast + +import com.google.android.material.snackbar.Snackbar +import io.xxlabs.messenger.R +import io.xxlabs.messenger.support.appContext + +interface ToastUI { + val backgroundColor: Int + val header: String? + val body: String? + val actionText: String? + val leftIcon: Int? + val iconTint: Int? + val duration: Int + + fun onActionClick() + + companion object Factory { + fun create( + body: String, + header: String? = null, + actionText: String? = null, + backgroundColor: Int = appContext().getColor(R.color.modal_overlay), + leftIcon: Int = R.drawable.ic_mail_sent, + iconTint: Int = R.color.neutral_white, + duration: Int = Snackbar.LENGTH_LONG, + actionClick: () -> Unit = {} + ) : ToastUI = object : ToastUI { + override val backgroundColor: Int = backgroundColor + override val header: String? = header + override val body: String? = body + override val actionText: String? = actionText + override val leftIcon: Int? = leftIcon + override val iconTint: Int? = iconTint + override val duration: Int = duration + + override fun onActionClick() = actionClick() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/support/util/CoroutineUtils.kt b/app/src/main/java/io/xxlabs/messenger/support/util/CoroutineUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..aa1d10c6738f4fdfa23db7275b9427cd48874ec1 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/support/util/CoroutineUtils.kt @@ -0,0 +1,30 @@ +package io.xxlabs.messenger.support.util + +import io.reactivex.Maybe +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +suspend inline fun <reified T> Single<T>.value( + scheduler: Scheduler = Schedulers.io() +): T = suspendCoroutine { continuation -> + this.subscribeOn(scheduler) + .observeOn(scheduler) + .doOnSuccess { result -> continuation.resume(result) } + .doOnError { error -> continuation.resumeWithException(error) } + .subscribe() +} + +suspend inline fun <reified T> Maybe<T>.value( + scheduler: Scheduler = Schedulers.io() +): T? = suspendCoroutine { continuation -> + this.subscribeOn(scheduler) + .observeOn(scheduler) + .doOnSuccess { result -> continuation.resume(result) } + .doOnError { error -> continuation.resumeWithException(error) } + .doOnComplete { continuation.resume(null) } + .subscribe() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/support/view/BitmapResolver.kt b/app/src/main/java/io/xxlabs/messenger/support/view/BitmapResolver.kt index ba178843af0e14b0bda48cf8a25e68ff347329e2..71a253225d1470dc8c2d9642fc1047c99bc77554 100644 --- a/app/src/main/java/io/xxlabs/messenger/support/view/BitmapResolver.kt +++ b/app/src/main/java/io/xxlabs/messenger/support/view/BitmapResolver.kt @@ -12,8 +12,10 @@ object BitmapResolver { return BitmapFactory.decodeStream(appContext().contentResolver.openInputStream(fileUri)) } - fun getBitmap(array: ByteArray): Bitmap? { - return BitmapFactory.decodeByteArray(array, 0, array.size) + fun getBitmap(array: ByteArray?): Bitmap? { + return array?.let { + BitmapFactory.decodeByteArray(array, 0, array.size) + } } /** diff --git a/app/src/main/java/io/xxlabs/messenger/support/view/SquaredCornerLayout.kt b/app/src/main/java/io/xxlabs/messenger/support/view/SquaredCornerLayout.kt index fac0fcfd42335dff3299f4f2cbf4d989fc4955ef..c3f6f5acc90767bc62a982a90d659eec5e6647df 100644 --- a/app/src/main/java/io/xxlabs/messenger/support/view/SquaredCornerLayout.kt +++ b/app/src/main/java/io/xxlabs/messenger/support/view/SquaredCornerLayout.kt @@ -14,7 +14,11 @@ import io.xxlabs.messenger.support.util.Utils class SquaredCornerLayout : FrameLayout { private val path = Path() private var rectF: RectF? = null - private val cornerRadius = Utils.dpToPx(32) + private var cornerRadius = Utils.dpToPx(32) + + fun setCornerRadius(dp: Int) { + cornerRadius = Utils.dpToPx(dp) + } constructor(context: Context?) : super(context!!) { defineBg() diff --git a/app/src/main/java/io/xxlabs/messenger/ui/ConfirmDialogLauncher.kt b/app/src/main/java/io/xxlabs/messenger/ui/ConfirmDialogLauncher.kt deleted file mode 100644 index 237e8ded1749e7e142fff2de53767cb2020dfdc1..0000000000000000000000000000000000000000 --- a/app/src/main/java/io/xxlabs/messenger/ui/ConfirmDialogLauncher.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.xxlabs.messenger.ui - -import androidx.fragment.app.FragmentManager -import io.xxlabs.messenger.support.appContext -import io.xxlabs.messenger.support.dialog.confirm.ConfirmDialog -import io.xxlabs.messenger.support.dialog.confirm.ConfirmDialogUI -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI - -class ConfirmDialogLauncher(private val fragmentMgr: FragmentManager) { - - fun showConfirmDialog( - title: Int, - body: Int, - button: Int, - action: () -> Unit, - onDismiss: () -> Unit = {} - ) { - val ui = ConfirmDialogUI.create( - infoDialogUI = InfoDialogUI.create( - title = appContext().getString(title), - body = appContext().getString(body), - null, - onDismiss - ), - buttonText = appContext().getString(button), - buttonOnClick = action - ) - ConfirmDialog.newInstance(ui) - .show(fragmentMgr, null) - } - - fun showConfirmDialog( - title: String, - body: String, - button: String, - action: () -> Unit, - onDismiss: () -> Unit = {} - ) { - val ui = ConfirmDialogUI.create( - infoDialogUI = InfoDialogUI.create( - title = title, - body = body, - null, - onDismiss - ), - buttonText = button, - buttonOnClick = action - ) - ConfirmDialog.newInstance(ui) - .show(fragmentMgr, null) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/base/BaseContactDetailsFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/base/BaseContactDetailsFragment.kt index f94f2165cd9fb0018aa6d9b32912671a76106819..9e31b0b8585635978722119e8b3da7579389399f 100755 --- a/app/src/main/java/io/xxlabs/messenger/ui/base/BaseContactDetailsFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/base/BaseContactDetailsFragment.kt @@ -15,9 +15,12 @@ import io.xxlabs.messenger.application.SchedulerProvider import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase import io.xxlabs.messenger.data.datatype.RequestStatus import io.xxlabs.messenger.data.room.model.ContactData +import io.xxlabs.messenger.data.room.model.formattedEmail +import io.xxlabs.messenger.data.room.model.formattedPhone import io.xxlabs.messenger.support.dialog.PopupActionBottomDialogFragment import io.xxlabs.messenger.support.extensions.* import io.xxlabs.messenger.support.util.DialogUtils +import io.xxlabs.messenger.ui.dialog.info.showInfoDialog import io.xxlabs.messenger.ui.global.ContactsViewModel import io.xxlabs.messenger.ui.global.NetworkViewModel import io.xxlabs.messenger.ui.main.MainActivity @@ -117,7 +120,7 @@ abstract class BaseContactDetailsFragment : BasePhotoFragment() { when (currContact.status) { RequestStatus.SEND_FAIL.value -> { Timber.v("Resending request auth channel...") - contactsViewModel.updateAndRequestAuthChannel(currContact.marshaled!!) + contactsViewModel.updateAndRequestAuthChannel(currContact) if (preferences.areInAppNotificationsOn) { (requireActivity() as MainActivity).createSnackMessage("Sending a new request") } @@ -250,8 +253,8 @@ abstract class BaseContactDetailsFragment : BasePhotoFragment() { private fun setData(contactBindings: ContactWrapperBase) { val username = contactBindings.getUsernameFact() - val email = contactBindings.getEmailFact() - val phone = contactBindings.getPhoneFact() + val email = contactBindings.getEmailFact(true) + val phone = contactBindings.getPhoneFact(true) if (username.isNotBlank()) { contactDetailsUsername.text = username @@ -292,8 +295,8 @@ abstract class BaseContactDetailsFragment : BasePhotoFragment() { private fun setData(contact: ContactData) { val username = contact.username - val email = contact.email - val phone = contact.phone + val email = contact.formattedEmail() + val phone = contact.formattedPhone(true) if (username.isNotBlank()) { contactDetailsUsername.text = username @@ -302,14 +305,14 @@ abstract class BaseContactDetailsFragment : BasePhotoFragment() { contactDetailsUsername.visibility = View.GONE } - if (email.isNotBlank()) { + if (!email.isNullOrBlank()) { contactDetailsEmail.text = email } else { contactDetailsEmailHeader.visibility = View.GONE contactDetailsEmail.visibility = View.GONE } - if (phone.isNotBlank()) { + if (!phone.isNullOrBlank()) { contactDetailsPhone.text = phone } else { contactDetailsPhoneHeader.visibility = View.GONE diff --git a/app/src/main/java/io/xxlabs/messenger/ui/base/BaseFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/base/BaseFragment.kt index 81c0c30a387943c2d2fe445c89b33f82e7a1ae12..6e275c80e5092871342e1aa7843f570d5d04e266 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/base/BaseFragment.kt @@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import dagger.android.support.AndroidSupportInjection +import io.xxlabs.messenger.R import io.xxlabs.messenger.bindings.wrapper.bindings.bindingsErrorMessage import io.xxlabs.messenger.biometrics.BiometricContainerCallback import io.xxlabs.messenger.biometrics.BiometricContainerProvider @@ -19,19 +20,9 @@ import io.xxlabs.messenger.di.utils.Injectable import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.support.appContext import io.xxlabs.messenger.support.dialog.PopupActionDialog -import io.xxlabs.messenger.support.dialog.action.ActionDialog -import io.xxlabs.messenger.support.dialog.action.ActionDialogUI -import io.xxlabs.messenger.support.dialog.confirm.ConfirmDialog -import io.xxlabs.messenger.support.dialog.confirm.ConfirmDialogUI -import io.xxlabs.messenger.support.dialog.info.InfoDialog -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI -import io.xxlabs.messenger.support.dialog.info.SpanConfig import io.xxlabs.messenger.support.extensions.setInsets import io.xxlabs.messenger.support.util.DialogUtils -import io.xxlabs.messenger.ui.ConfirmDialogLauncher import io.xxlabs.messenger.ui.main.MainActivity -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialog -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialogUI import io.xxlabs.messenger.ui.main.qrcode.QrCodeScanFragment import javax.inject.Inject @@ -49,10 +40,6 @@ abstract class BaseFragment : Fragment(), Injectable { ) } - private val confirmDialogLauncher: ConfirmDialogLauncher by lazy { - ConfirmDialogLauncher(requireActivity().supportFragmentManager) - } - private val biometricContainerCallback by lazy { object : BiometricContainerCallback { override fun onBiometricsNotAvailable() { biometricContainerProvider.showEnableBiometrics() } @@ -150,110 +137,10 @@ abstract class BaseFragment : Fragment(), Injectable { fun showError(text: String, isBindingError: Boolean = false) = showError(Exception(text), isBindingError) - protected fun showInfoDialog( - title: Int, - body: Int, - linkTextToUrlMap: Map<String, String>? = null - ) { - var spans: MutableList<SpanConfig>? = null - linkTextToUrlMap?.apply { - spans = mutableListOf() - for (entry in keys) { - val spanConfig = SpanConfig.create( - entry, - this[entry], - ) - spans?.add(spanConfig) - } - } - val ui = InfoDialogUI.create( - title = getString(title), - body = getString(body), - spans = spans, - ) - InfoDialog.newInstance(ui) - .show(requireActivity().supportFragmentManager, null) - } - - protected fun showTwoButtonInfoDialog( - title: Int, - body: Int, - linkTextToUrlMap: Map<String, String>? = null, - positiveClick: ()-> Unit, - negativeClick: (()-> Unit)? = null, - onDismiss: ()-> Unit = { }, - ) { - var spans: MutableList<SpanConfig>? = null - linkTextToUrlMap?.apply { - spans = mutableListOf() - for (entry in keys) { - val spanConfig = SpanConfig.create( - entry, - this[entry], - ) - spans?.add(spanConfig) - } - } - val infoDialogUI = InfoDialogUI.create( - title = getString(title), - body = getString(body), - spans = spans, - onDismiss - ) - val twoButtonUI = TwoButtonInfoDialogUI.create( - infoDialogUI, - onPositiveClick = positiveClick, - onNegativeClick = negativeClick - ) - TwoButtonInfoDialog.newInstance(twoButtonUI) - .show(requireActivity().supportFragmentManager, null) - } - - protected fun showConfirmDialog( - title: Int, - body: Int, - button: Int, - action: () -> Unit, - onDismiss: () -> Unit = {} - ) { - confirmDialogLauncher.showConfirmDialog(title, body, button, action, onDismiss) - } - - protected fun showConfirmDialog( - title: String, - body: String, - button: String, - action: () -> Unit, - onDismiss: () -> Unit = {} - ) { - confirmDialogLauncher.showConfirmDialog(title, body, button, action, onDismiss) - } - - protected fun showActionDialog( - title: Int, - body: Int, - button: Int, - action: () -> Unit - ) { - val ui = ActionDialogUI.create( - ConfirmDialogUI.create( - infoDialogUI = InfoDialogUI.create( - title = getString(title), - body = getString(body), - ), - buttonText = getString(button), - buttonOnClick = action - ) - ) - ActionDialog.newInstance(ui) - .show(requireActivity().supportFragmentManager, null) - } - protected fun openSettings() { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri: Uri = Uri.fromParts("package", appContext().packageName, null) intent.data = uri startActivityForResult(intent, QrCodeScanFragment.cameraPermissionRequestCode) } - } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/base/BasePhotoFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/base/BasePhotoFragment.kt index 30b175d6e7bc0e9e26e7e5818f29f9b2a0789036..c088c5ab4fb71ddd2a0212e69de3fe986d25ce94 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/base/BasePhotoFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/base/BasePhotoFragment.kt @@ -2,6 +2,7 @@ package io.xxlabs.messenger.ui.base import android.Manifest import android.app.Activity +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.res.Resources @@ -25,6 +26,10 @@ import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.google.firebase.crashlytics.FirebaseCrashlytics import io.xxlabs.messenger.R +import io.xxlabs.messenger.media.CameraProvider +import io.xxlabs.messenger.media.DeviceStorageProvider +import io.xxlabs.messenger.media.MediaCallback +import io.xxlabs.messenger.media.MicrophoneProvider import io.xxlabs.messenger.support.dialog.PopupActionDialog import io.xxlabs.messenger.support.extensions.toast import io.xxlabs.messenger.support.util.FileUtils @@ -34,14 +39,30 @@ import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException +import java.lang.ClassCastException import java.text.SimpleDateFormat import java.util.* -abstract class BasePhotoFragment(val layout: Int = R.layout.fragment_contact_details) : - BaseFragment() { +abstract class BasePhotoFragment( + val layout: Int = R.layout.fragment_contact_details +) : BaseFragment(), MediaCallback { protected lateinit var root: View lateinit var currentPhotoPath: String + /* Camera and gallery access */ + + private lateinit var cameraProvider: CameraProvider + private lateinit var galleryProvider: DeviceStorageProvider + + override fun onAttach(context: Context) { + super.onAttach(context) + + cameraProvider = context as? CameraProvider + ?: throw ClassCastException("Activity must implement CameraProvider!") + galleryProvider = context as? DeviceStorageProvider + ?: throw ClassCastException("Activity must implement DeviceStorageProvider!") + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -173,17 +194,26 @@ abstract class BasePhotoFragment(val layout: Int = R.layout.fragment_contact_det } protected fun requestPermissionChoosePhoto() { - FileUtils.checkPermissionDo( - this, - REQUEST_CODE_WRITE_EXTERNAL_STORAGE, - { openGallery() }) + galleryProvider.selectFiles( + callback = this, + mimeTypes = listOf("image/*"), + multipleSelections = false + ) + } + + override fun onFilesSelected(uriList: List<Uri>) { + if (uriList.isNotEmpty()) { + try { + val bitmap = BitmapResolver.getBitmap(uriList.first()) + loadBitmap(bitmap) + } catch (e: Exception) { + showError("An error occurred, please try again.") + } + } } protected fun requestPermissionTakePhoto() { - FileUtils.checkPermissionDo( - this, - REQUEST_CODE_CAMERA, { takePhoto() }, true - ) + cameraProvider.startCamera(this, false) } @Throws(IOException::class) diff --git a/app/src/main/java/io/xxlabs/messenger/ui/base/BaseProfileRegistrationFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/base/BaseProfileRegistrationFragment.kt index 461d786ea77aa54be915dd04d4353ef1521c7d69..2aa45c3039dbc1bc1c070ddfd373114dfc292ccb 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/base/BaseProfileRegistrationFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/base/BaseProfileRegistrationFragment.kt @@ -356,7 +356,7 @@ abstract class BaseProfileRegistrationFragment(val isRegistration: Boolean = fal } val text = SpannableStringBuilder("Resend code ").append("($remainingSeconds secs)") textView?.text = text - textView?.contentDescription = "ud.profile.dialog.resend.counting" + textView?.contentDescription = "ud.profile.dialog.send.counting" Timber.v("[VERIFICATION CODE]Remaining seconds: $remainingSeconds") } @@ -369,7 +369,7 @@ abstract class BaseProfileRegistrationFragment(val isRegistration: Boolean = fal ) ) } - textView?.contentDescription = "ud.profile.dialog.resend.ready" + textView?.contentDescription = "ud.profile.dialog.send.ready" textView?.text = "Resend Code" textView?.enable() } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/base/ContactDetailsViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/base/ContactDetailsViewModel.kt index 4bb20129f2d0c78214a2d5aaba94ff0786219601..f764ff22a1c4e9e488d4efa292285568fd22c8e6 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/base/ContactDetailsViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/base/ContactDetailsViewModel.kt @@ -25,7 +25,7 @@ class ContactDetailsViewModel @Inject constructor( val contactData = MutableLiveData<ContactData?>() val deletedChat = MutableLiveData<DataRequestState<Boolean>>() val deletedContact = MutableLiveData<DataRequestState<Boolean>>() - var searchState = MutableLiveData<DataRequestState<ByteArray>>() + var searchState = MutableLiveData<DataRequestState<ContactData>>() private var subscriptions = CompositeDisposable() var currContact: ContactData? = null @@ -176,7 +176,7 @@ class ContactDetailsViewModel @Inject constructor( onSuccess = { id -> contact.id = id Timber.v("Successfully requested authenticated channel") - searchState.postValue(DataRequestState.Success(marshalledContact)) + searchState.postValue(DataRequestState.Success(contact)) }) ) } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/dialog/ExpandedBottomSheetDialog.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/ExpandedBottomSheetDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..14e75d80a6a7e02a472eb03e01fdb66ac3a32437 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/ExpandedBottomSheetDialog.kt @@ -0,0 +1,18 @@ +package io.xxlabs.messenger.ui.dialog + +import android.os.Bundle +import android.view.View +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import io.xxlabs.messenger.R + +abstract class ExpandedBottomSheetDialog : BottomSheetDialogFragment() { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun getTheme(): Int = R.style.RoundedModalBottomSheetDialog +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/support/dialog/action/ActionDialog.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialog.kt similarity index 97% rename from app/src/main/java/io/xxlabs/messenger/support/dialog/action/ActionDialog.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialog.kt index 64ca08e2089e8c318e0f6f8e458db343897fbfa5..12729cf93d763219d7e1413bb46c764bab97e7f0 100644 --- a/app/src/main/java/io/xxlabs/messenger/support/dialog/action/ActionDialog.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialog.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.support.dialog.action +package io.xxlabs.messenger.ui.dialog.action import android.content.DialogInterface import android.os.Bundle diff --git a/app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialogLauncher.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialogLauncher.kt new file mode 100644 index 0000000000000000000000000000000000000000..6826a1a0aca7c6ef16680839ad98c4cc3252bdec --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialogLauncher.kt @@ -0,0 +1,29 @@ +package io.xxlabs.messenger.ui.dialog.action + +import androidx.fragment.app.Fragment +import io.xxlabs.messenger.ui.dialog.warning.WarningDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI + +/** + * Launches an ActionDialog with a positive button. + */ +@Deprecated("Fragments should receive the DialogUI from their ViewModel.") +fun Fragment.showActionDialog( + title: Int, + body: Int, + button: Int, + action: () -> Unit +) { + val ui = ActionDialogUI.create( + WarningDialogUI.create( + infoDialogUI = InfoDialogUI.create( + title = getString(title), + body = getString(body), + ), + buttonText = getString(button), + buttonOnClick = action + ) + ) + ActionDialog.newInstance(ui) + .show(parentFragmentManager, null) +} diff --git a/app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialogUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialogUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..4fe404172d32598c5b0c19f3a6e895484eccf4fa --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/action/ActionDialogUI.kt @@ -0,0 +1,12 @@ +package io.xxlabs.messenger.ui.dialog.action + +import io.xxlabs.messenger.ui.dialog.warning.WarningDialogUI + +interface ActionDialogUI : WarningDialogUI { + + companion object Factory { + fun create(warningDialogUI: WarningDialogUI) : ActionDialogUI { + return object : ActionDialogUI, WarningDialogUI by warningDialogUI {} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/dialog/components/CloseButtonUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/components/CloseButtonUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..e307525e3fa37a4fe6696c0ee8d53c732f1b3929 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/components/CloseButtonUI.kt @@ -0,0 +1,5 @@ +package io.xxlabs.messenger.ui.dialog.components + +interface CloseButtonUI { + fun onCloseClicked() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/dialog/components/PostiiveNegativeButtonUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/components/PostiiveNegativeButtonUI.kt new file mode 100644 index 0000000000000000000000000000000000000000..fbe842124446e895798f40973825cff3bbb2f7c1 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/components/PostiiveNegativeButtonUI.kt @@ -0,0 +1,12 @@ +package io.xxlabs.messenger.ui.dialog.components + +import androidx.lifecycle.LiveData + +interface PositiveNegativeButtonUI { + val positiveLabel: Int + val negativeLabel: Int + val positiveButtonEnabled: LiveData<Boolean> + + fun onPositiveClick() + fun onNegativeClick() +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/support/dialog/info/InfoDialog.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialog.kt similarity index 98% rename from app/src/main/java/io/xxlabs/messenger/support/dialog/info/InfoDialog.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialog.kt index 5b638b406173b6d2c0f8ff0ec2847f610e6af86b..51e90a90caa2dbed6a90049b00620085d4c0e44b 100644 --- a/app/src/main/java/io/xxlabs/messenger/support/dialog/info/InfoDialog.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialog.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.support.dialog.info +package io.xxlabs.messenger.ui.dialog.info import android.content.DialogInterface import android.os.Bundle diff --git a/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogLauncher.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogLauncher.kt new file mode 100644 index 0000000000000000000000000000000000000000..99f5cd93980aee5bc0d49c8da204a2be72a71618 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogLauncher.kt @@ -0,0 +1,70 @@ +package io.xxlabs.messenger.ui.dialog.info + +import androidx.fragment.app.Fragment + +/** + * Launches an InfoDialog with a neutral button. + */ +@Deprecated("Fragments should receive the DialogUI from their ViewModel.") +fun Fragment.showInfoDialog( + title: Int, + body: Int, + linkTextToUrlMap: Map<String, String>? = null +) { + var spans: MutableList<SpanConfig>? = null + linkTextToUrlMap?.apply { + spans = mutableListOf() + for (entry in keys) { + val spanConfig = SpanConfig.create( + entry, + this[entry], + ) + spans?.add(spanConfig) + } + } + val ui = InfoDialogUI.create( + title = getString(title), + body = getString(body), + spans = spans, + ) + InfoDialog.newInstance(ui) + .show(requireActivity().supportFragmentManager, null) +} + +/** + * Launches an InfoDialog with a positive and negative button. + */ +@Deprecated("Fragments should receive the DialogUI from their ViewModel.") +fun Fragment.showTwoButtonInfoDialog( + title: Int, + body: Int, + linkTextToUrlMap: Map<String, String>? = null, + positiveClick: ()-> Unit, + negativeClick: (()-> Unit)? = null, + onDismiss: ()-> Unit = { }, +) { + var spans: MutableList<SpanConfig>? = null + linkTextToUrlMap?.apply { + spans = mutableListOf() + for (entry in keys) { + val spanConfig = SpanConfig.create( + entry, + this[entry], + ) + spans?.add(spanConfig) + } + } + val infoDialogUI = InfoDialogUI.create( + title = getString(title), + body = getString(body), + spans = spans, + onDismiss + ) + val twoButtonUI = TwoButtonInfoDialogUI.create( + infoDialogUI, + onPositiveClick = positiveClick, + onNegativeClick = negativeClick + ) + TwoButtonInfoDialog.newInstance(twoButtonUI) + .show(parentFragmentManager, null) +} diff --git a/app/src/main/java/io/xxlabs/messenger/support/dialog/info/InfoDialogUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogUI.kt similarity index 96% rename from app/src/main/java/io/xxlabs/messenger/support/dialog/info/InfoDialogUI.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogUI.kt index f27618c122d6fd943a37fbc34b6072ae59eef049..58aa88d24e028dd6b1635a545b7df6585c4762fa 100644 --- a/app/src/main/java/io/xxlabs/messenger/support/dialog/info/InfoDialogUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogUI.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.support.dialog.info +package io.xxlabs.messenger.ui.dialog.info import io.xxlabs.messenger.R import java.io.Serializable diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/chats/TwoButtonInfoDialog.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/TwoButtonInfoDialog.kt similarity index 97% rename from app/src/main/java/io/xxlabs/messenger/ui/main/chats/TwoButtonInfoDialog.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/info/TwoButtonInfoDialog.kt index 852f2af69396cf86b439f2251248f1d6493d0563..e8562d02712df47f13ab87cab9056230baebe69b 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/chats/TwoButtonInfoDialog.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/TwoButtonInfoDialog.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.ui.main.chats +package io.xxlabs.messenger.ui.dialog.info import android.content.DialogInterface import android.os.Bundle @@ -16,7 +16,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import io.xxlabs.messenger.R import io.xxlabs.messenger.databinding.ComponentTwoButtonDialogBinding -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI class TwoButtonInfoDialog : BottomSheetDialogFragment() { diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/chats/TwoButtonInfoDialogUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/TwoButtonInfoDialogUI.kt similarity index 85% rename from app/src/main/java/io/xxlabs/messenger/ui/main/chats/TwoButtonInfoDialogUI.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/info/TwoButtonInfoDialogUI.kt index a0ba91388cc1553590257cf1815657b1b14f9f89..68b5010ab975a3d8bc534840606c2b74e4e5d0a2 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/chats/TwoButtonInfoDialogUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/TwoButtonInfoDialogUI.kt @@ -1,13 +1,12 @@ -package io.xxlabs.messenger.ui.main.chats +package io.xxlabs.messenger.ui.dialog.info import io.xxlabs.messenger.R -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI interface TwoButtonInfoDialogUI : InfoDialogUI { val positiveLabel: Int val negativeLabel: Int val onPositiveClick: () -> Unit - val onNegativeClick: (() -> Unit) + val onNegativeClick: () -> Unit companion object Factory { fun create( diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/RadioButtonDialog.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/radiobutton/RadioButtonDialog.kt similarity index 96% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/RadioButtonDialog.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/radiobutton/RadioButtonDialog.kt index 80f4e3268d3c5166f6576a9fb509d8cd38ad4aca..c8132508a24c304636667620a17237965322f31d 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/RadioButtonDialog.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/radiobutton/RadioButtonDialog.kt @@ -1,11 +1,10 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.ui.dialog.radiobutton import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.RadioButton -import androidx.core.view.marginTop import androidx.core.view.postDelayed import androidx.databinding.DataBindingUtil import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -13,7 +12,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import io.xxlabs.messenger.R import io.xxlabs.messenger.databinding.ComponentRadiobuttonDialogBinding -import kotlinx.coroutines.* class RadioButtonDialog : BottomSheetDialogFragment() { diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/RadioButtonDialogUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/radiobutton/RadioButtonDialogUI.kt similarity index 94% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/RadioButtonDialogUI.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/radiobutton/RadioButtonDialogUI.kt index a51398f14551937d4a6a7174245c199d85ca5886..488531dc959a6a6c31fb492aabe0cd34801ff488 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/RadioButtonDialogUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/radiobutton/RadioButtonDialogUI.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.ui.dialog.radiobutton import java.io.Serializable diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/EditTextTwoButtonInfoDialog.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/textinput/TextInputDialog.kt similarity index 83% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/EditTextTwoButtonInfoDialog.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/textinput/TextInputDialog.kt index 2728f72772b5b898fb224f0a0733976dee16f525..9dc547a2a8ab7374131d4b770ddd383f442ad440 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/EditTextTwoButtonInfoDialog.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/textinput/TextInputDialog.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.ui.dialog.textinput import android.content.DialogInterface import android.os.Bundle @@ -15,14 +15,14 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import io.xxlabs.messenger.R -import io.xxlabs.messenger.databinding.ComponentEdittextTwoButtonDialogBinding -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.databinding.ComponentTextinputDialogBinding +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI -class EditTextTwoButtonInfoDialog : BottomSheetDialogFragment() { +class TextInputDialog : BottomSheetDialogFragment() { - private lateinit var binding: ComponentEdittextTwoButtonDialogBinding - private val dialogUI: EditTextTwoButtonDialogUI by lazy { - requireArguments().get(ARG_UI) as EditTextTwoButtonDialogUI + private lateinit var binding: ComponentTextinputDialogBinding + private val dialogUI: TextInputDialogUI by lazy { + requireArguments().get(ARG_UI) as TextInputDialogUI } override fun onCreateView( @@ -32,7 +32,7 @@ class EditTextTwoButtonInfoDialog : BottomSheetDialogFragment() { ): View { binding = DataBindingUtil.inflate( inflater, - R.layout.component_edittext_two_button_dialog, + R.layout.component_textinput_dialog, container, false ) @@ -106,8 +106,8 @@ class EditTextTwoButtonInfoDialog : BottomSheetDialogFragment() { companion object Factory { private const val ARG_UI: String = "ui" - fun newInstance(dialogUI: EditTextTwoButtonDialogUI): EditTextTwoButtonInfoDialog = - EditTextTwoButtonInfoDialog().apply { + fun newInstance(dialogUI: TextInputDialogUI): TextInputDialog = + TextInputDialog().apply { arguments = Bundle().apply { putSerializable(ARG_UI, dialogUI) } diff --git a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/EditTextTwoButtonDialogUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/textinput/TextInputDialogUI.kt similarity index 79% rename from app/src/main/java/io/xxlabs/messenger/backup/ui/save/EditTextTwoButtonDialogUI.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/textinput/TextInputDialogUI.kt index 7c67afd18a8159c1357434f442cc9a6c3e8afe79..14c285ca4a665ce6a585dac65c115ce75dcdc237 100644 --- a/app/src/main/java/io/xxlabs/messenger/backup/ui/save/EditTextTwoButtonDialogUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/textinput/TextInputDialogUI.kt @@ -1,10 +1,10 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.ui.dialog.textinput import android.text.Editable import androidx.lifecycle.LiveData -import io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI -interface EditTextTwoButtonDialogUI : TwoButtonInfoDialogUI { +interface TextInputDialogUI : TwoButtonInfoDialogUI { val maxInputLength: Int val inputHint: Int val inputError: LiveData<String?> @@ -19,8 +19,8 @@ interface EditTextTwoButtonDialogUI : TwoButtonInfoDialogUI { positiveButtonEnabled: LiveData<Boolean>, onTextInput: (Editable) -> Unit, twoButtonInfoDialogUI: TwoButtonInfoDialogUI, - ): EditTextTwoButtonDialogUI { - return object : EditTextTwoButtonDialogUI, + ): TextInputDialogUI { + return object : TextInputDialogUI, TwoButtonInfoDialogUI by twoButtonInfoDialogUI { override val maxInputLength: Int = maxInputLength override val inputHint: Int = inputHint diff --git a/app/src/main/java/io/xxlabs/messenger/support/dialog/confirm/ConfirmDialog.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialog.kt similarity index 76% rename from app/src/main/java/io/xxlabs/messenger/support/dialog/confirm/ConfirmDialog.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialog.kt index 1e9c14d62d9e803216355e07d133fa44821f261f..89e5e0fd59c60507631462a0b122a1ca4132155b 100644 --- a/app/src/main/java/io/xxlabs/messenger/support/dialog/confirm/ConfirmDialog.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialog.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.support.dialog.confirm +package io.xxlabs.messenger.ui.dialog.warning import android.content.DialogInterface import android.os.Bundle @@ -10,13 +10,13 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import io.xxlabs.messenger.R -import io.xxlabs.messenger.databinding.ComponentConfirmDialogBinding +import io.xxlabs.messenger.databinding.ComponentWarningDialogBinding -class ConfirmDialog : BottomSheetDialogFragment() { +class WarningDialog : BottomSheetDialogFragment() { - private lateinit var binding: ComponentConfirmDialogBinding - private val dialogUI: ConfirmDialogUI by lazy { - requireArguments().getSerializable(ARG_UI) as ConfirmDialogUI + private lateinit var binding: ComponentWarningDialogBinding + private val dialogUI: WarningDialogUI by lazy { + requireArguments().getSerializable(ARG_UI) as WarningDialogUI } override fun onCreateView( @@ -26,7 +26,7 @@ class ConfirmDialog : BottomSheetDialogFragment() { ): View { binding = DataBindingUtil.inflate( inflater, - R.layout.component_confirm_dialog, + R.layout.component_warning_dialog, container, false ) @@ -56,8 +56,8 @@ class ConfirmDialog : BottomSheetDialogFragment() { companion object Factory { private const val ARG_UI: String = "ui" - fun newInstance(dialogUI: ConfirmDialogUI): ConfirmDialog = - ConfirmDialog().apply { + fun newInstance(dialogUI: WarningDialogUI): WarningDialog = + WarningDialog().apply { arguments = Bundle().apply { putSerializable(ARG_UI, dialogUI) } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialogLauncher.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialogLauncher.kt new file mode 100644 index 0000000000000000000000000000000000000000..b6e3c1d7ab7a9021533f53beedec68899caa7ae5 --- /dev/null +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialogLauncher.kt @@ -0,0 +1,56 @@ +package io.xxlabs.messenger.ui.dialog.warning + +import androidx.fragment.app.Fragment +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI + +/** + * Launches an ConfirmDialog with a positive button. + */ +@Deprecated("Fragments should receive the DialogUI from their ViewModel.") +fun Fragment.showConfirmDialog( + title: Int, + body: Int, + button: Int, + action: () -> Unit, + onDismiss: () -> Unit = {} +) { + val ui = WarningDialogUI.create( + infoDialogUI = InfoDialogUI.create( + title = requireContext().getString(title), + body = requireContext().getString(body), + null, + onDismiss + ), + buttonText = requireContext().getString(button), + buttonOnClick = action + ) + WarningDialog + .newInstance(ui) + .show(parentFragmentManager, null) +} + +/** + * Launches an ConfirmDialog with a positive button. + */ +@Deprecated("Fragments should receive the DialogUI from their ViewModel.") +fun Fragment.showConfirmDialog( + title: String, + body: String, + button: String, + action: () -> Unit, + onDismiss: () -> Unit = {} +) { + val ui = WarningDialogUI.create( + infoDialogUI = InfoDialogUI.create( + title = title, + body = body, + null, + onDismiss + ), + buttonText = button, + buttonOnClick = action + ) + WarningDialog + .newInstance(ui) + .show(parentFragmentManager, null) +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/support/dialog/confirm/ConfirmDialogUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialogUI.kt similarity index 60% rename from app/src/main/java/io/xxlabs/messenger/support/dialog/confirm/ConfirmDialogUI.kt rename to app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialogUI.kt index ca71c224dd97bcd4df0bd23e1e946ffdae48e4c8..ac98e665ed82134224ef160a56220c290eccf940 100644 --- a/app/src/main/java/io/xxlabs/messenger/support/dialog/confirm/ConfirmDialogUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/warning/WarningDialogUI.kt @@ -1,8 +1,8 @@ -package io.xxlabs.messenger.support.dialog.confirm +package io.xxlabs.messenger.ui.dialog.warning -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI -interface ConfirmDialogUI : InfoDialogUI { +interface WarningDialogUI : InfoDialogUI { val buttonText: String val buttonOnClick: () -> Unit @@ -11,8 +11,8 @@ interface ConfirmDialogUI : InfoDialogUI { infoDialogUI: InfoDialogUI, buttonText: String, buttonOnClick: () -> Unit - ): ConfirmDialogUI { - return object : ConfirmDialogUI, InfoDialogUI by infoDialogUI { + ): WarningDialogUI { + return object : WarningDialogUI, InfoDialogUI by infoDialogUI { override val buttonText = buttonText override val buttonOnClick = buttonOnClick } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/global/ContactsViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/global/ContactsViewModel.kt index d289316f79ff43101444c747ce17dc701319b184..67a56e4567d7ac7759e7674c583ccf3faf031cbe 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/global/ContactsViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/global/ContactsViewModel.kt @@ -4,44 +4,64 @@ import android.app.Application import android.util.Pair import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import io.reactivex.Single import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.subscribeBy +import io.xxlabs.messenger.BuildConfig +import io.xxlabs.messenger.R import io.xxlabs.messenger.application.SchedulerProvider import io.xxlabs.messenger.application.XxMessengerApplication import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperMock -import io.xxlabs.messenger.data.data.ContactRoundRequest import io.xxlabs.messenger.data.data.DataRequestState import io.xxlabs.messenger.data.data.PayloadWrapper import io.xxlabs.messenger.data.data.SimpleRequestState -import io.xxlabs.messenger.data.datatype.ContactRequestState import io.xxlabs.messenger.data.datatype.RequestStatus import io.xxlabs.messenger.data.datatype.FactType import io.xxlabs.messenger.data.datatype.MessageStatus +import io.xxlabs.messenger.data.datatype.RequestStatus.* import io.xxlabs.messenger.data.room.model.* import io.xxlabs.messenger.repository.DaoRepository import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.repository.base.BaseRepository import io.xxlabs.messenger.repository.client.ClientRepository +import io.xxlabs.messenger.requests.data.contact.ContactRequestData +import io.xxlabs.messenger.requests.data.contact.ContactRequestsRepository +import io.xxlabs.messenger.requests.data.contact.RequestMigrator +import io.xxlabs.messenger.requests.data.group.InvitationMigrator import io.xxlabs.messenger.support.extensions.combineWith import io.xxlabs.messenger.support.extensions.toBase64String import io.xxlabs.messenger.support.isMockVersion +import io.xxlabs.messenger.support.toast.ToastUI import io.xxlabs.messenger.support.util.Utils +import io.xxlabs.messenger.support.util.value +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject class ContactsViewModel @Inject constructor( - val app: Application, - val repo: BaseRepository, - val daoRepo: DaoRepository, - val preferences: PreferencesRepository, - val schedulers: SchedulerProvider, + private val app: Application, + private val repo: BaseRepository, + private val daoRepo: DaoRepository, + private val preferences: PreferencesRepository, + private val schedulers: SchedulerProvider, + private val requestsDataSource: ContactRequestsRepository ) : ViewModel() { + + val showToast: Flow<ToastUI?> by ::_showToast + private val _showToast = MutableStateFlow<ToastUI?>(null) + + val navigateToChat: Flow<Contact?> by ::_navigateToChat + private val _navigateToChat = MutableStateFlow<Contact?>(null) + var subscriptions = CompositeDisposable() var newAuthRequestSent = MutableLiveData<SimpleRequestState<Any>>() var newConfirmRequestSent = MutableLiveData<DataRequestState<Any>>() - var newIncomingRequestReceived = MutableLiveData<SimpleRequestState<ByteArray>>() + var newIncomingRequestReceived = MutableLiveData<SimpleRequestState<ByteArray?>>() var newConfirmationRequestReceived = MutableLiveData<SimpleRequestState<ByteArray>>() var newGroupRequestSent = MutableLiveData<DataRequestState<Boolean>>() var acceptGroupRequest = MutableLiveData<DataRequestState<GroupData>>() @@ -49,39 +69,29 @@ class ContactsViewModel @Inject constructor( val contactsData = daoRepo.getAllContactsLive() val groupsData = daoRepo.getAllGroupsLive() - val requestsCount = MutableLiveData<Int>() + val requestsCount = requestsDataSource.unreadCount.asLiveData() val combinedContactGroupsData = contactsData.combineWith(groupsData) { contacts, groups -> Pair(contacts ?: listOf(), groups ?: listOf()) } init { Timber.v("isAuthCallbackRegistered: ${isAuthCallbackRegistered()}") - requestsCount.value = preferences.contactsCount + migrateOldRequests() + if (BuildConfig.DEBUG) listContacts() } - fun addRequestCount() { - val currVal = preferences.contactsCount - Timber.v("Current requests val $currVal") - preferences.contactsCount = currVal + 1 - requestsCount.postValue(preferences.contactsCount) - } - - fun viewAllRequests() { - val currVal = preferences.contactsCount - Timber.v("Current val $currVal") - preferences.contactsCount = 0 - requestsCount.value = 0 + private fun listContacts() { + viewModelScope.launch { + daoRepo.getAllContacts().value().forEach { contactData -> + Timber.d("Found contact: ${contactData.displayName}/${contactData.userId.toBase64String()}") + } + } } - fun viewSingleRequest() { - val currVal = preferences.contactsCount - Timber.v("Current val $currVal") - if (currVal > 0) { - preferences.contactsCount = currVal - 1 - } else { - preferences.contactsCount = 0 + private fun migrateOldRequests() { + viewModelScope.launch { + RequestMigrator.performMigration(preferences, requestsDataSource, daoRepo) } - requestsCount.value = preferences.contactsCount } fun registerAuthCallback() { @@ -116,7 +126,6 @@ class ContactsViewModel @Inject constructor( private fun onConfirmationReceived(contact: ByteArray) { val id = getBindingsContactId(contact) Timber.v("Request feedback received from: ${id.toBase64String()}") - newConfirmationRequestReceived.postValue(SimpleRequestState.Success(contact)) confirmRequest(contact) } @@ -132,91 +141,80 @@ class ContactsViewModel @Inject constructor( "" ) if (roundId > 0) { - updateContactStatus(contact.userId, RequestStatus.RESET_SENT) - preferences.removeContactRequests(contact.userId) - preferences.addContactRequest( - contact.userId, - contact.username, - roundId, - true - ) - + updateContactStatus(contact.userId, RESET_SENT) + saveRequest(contact) } } catch (e: Exception) { Timber.d("Failed to reset session: ${e.message}") } } - fun updateAndRequestAuthChannel(contact: ByteArray) { + fun updateAndRequestAuthChannel(contact: ContactData) { if (isMockVersion()) { newAuthRequestSent.postValue(SimpleRequestState.Success(Any())) return } - requestAuthenticatedChannel(contact) } - fun requestAuthenticatedChannel(contact: ByteArray) { - val contactWrapper = repo.unmarshallContact(contact) + private fun requestAuthenticatedChannel(contact: ContactData) { + val contactWrapper = repo.unmarshallContact(contact.marshaled!!) val bindingsId = contactWrapper!!.getId() subscriptions.add( - repo.requestAuthenticatedChannel(contact) + repo.requestAuthenticatedChannel(contact.marshaled!!) .subscribeOn(schedulers.io) .observeOn(schedulers.io) .doOnSuccess { roundId -> if (isMockVersion()) { updateContactMock(bindingsId, contactWrapper) - Timber.v("${contact.toBase64String()} became ${(contactWrapper as ContactWrapperMock).contact.status}!") + Timber.v("${contact.displayName} became ${(contactWrapper as ContactWrapperMock).contact.status}!") newAuthRequestSent.postValue(SimpleRequestState.Success(Any())) } else { Timber.v("contact request sent to: ${bindingsId.toBase64String()}") - updateContactStatus( - bindingsId, - RequestStatus.SENT - ) - - val request = preferences.getContactRequest(bindingsId) - if (request != null && request.isSent) { - preferences.removeContactRequest(request) - } - - val username = contactWrapper.getUsernameFact() - preferences.addContactRequest( - bindingsId, - username, - roundId, - true - ) - - preferences.getContactRequest(bindingsId)?.apply { - completeRound(this) - } -// waitForRoundCompletion(bindingsId, roundId) + saveContact(contact, SENT) + saveRequest(contact) newAuthRequestSent.postValue(SimpleRequestState.Success(Any())) } } .doOnError { err -> Timber.e("Request error for ${bindingsId.toBase64String()}: ${err.localizedMessage}") - newAuthRequestSent.postValue(SimpleRequestState.Error()) + newAuthRequestSent.postValue(SimpleRequestState.Error( + Exception("Your contact request to ${contact.displayName} has failed.") + )) handleRequestAuthChannelError(err, contactWrapper) }.subscribe() ) } + private fun saveContact(contact: ContactData, status: RequestStatus) { + viewModelScope.launch { + try { + if (daoRepo.getContactByUserId(contact.userId).value() == null) { + val rowId = daoRepo.addNewContact(contact.copy(status = status.value)).value() + Timber.d("Saved ${contact.displayName} to row $rowId in DB.") + } else { + updateContactStatus(contact.userId, status) { + Timber.d("Updated ${contact.displayName} with status $status in DB.") + } + } + } catch (e: Exception) { + Timber.d("Exception saving ${contact.displayName}: ${e.message}") + } + } + } + fun confirmAuthenticatedChannel(contact: ContactData) { val contactWrapper = repo.unmarshallContact(contact.marshaled!!)!! contactWrapper.addUsername(contact.username) val bindingsId = getBindingsContactId(contactWrapper.marshal()) Timber.v("Confirming authentication channel with ${bindingsId.toBase64String()}") newConfirmRequestSent.postValue(DataRequestState.Start()) - var roundId: Long = -1L subscriptions.add( repo.confirmAuthenticatedChannel(contactWrapper.marshal()) .subscribeOn(schedulers.single) .observeOn(schedulers.io) - .flatMap { newRoundId -> - roundId = newRoundId + .flatMap { roundId -> daoRepo.updateContact(contact) } .observeOn(schedulers.main) @@ -225,7 +223,7 @@ class ContactsViewModel @Inject constructor( Timber.v("Authentication channel confirm sent! wait for its round to complete...") updateContactStatus( contactWrapper.getId(), - requestStatus = RequestStatus.ACCEPTED + requestStatus = ACCEPTED ) newConfirmRequestSent.postValue(DataRequestState.Success(contact)) return@doOnSuccess @@ -233,27 +231,16 @@ class ContactsViewModel @Inject constructor( Timber.v("Authentication channel confirm sent! wait for its round to complete...") newConfirmRequestSent.postValue(DataRequestState.Success(Any())) - - preferences.removeContactRequests(contact.userId) - - preferences.addContactRequest( - bindingsId, - contactWrapper.getUsernameFact(), - roundId, - false - ) - - preferences.getContactRequest(bindingsId)?.apply { - completeRound(this) - } + deleteRequest(contact) }.doOnError { err -> Timber.e("Request error for ${bindingsId.toBase64String()}: ${err.localizedMessage}") updateContactStatus( contactWrapper.getId(), - RequestStatus.CONFIRM_FAIL + CONFIRM_FAIL ) - roundRequestFail(contactWrapper) - newConfirmRequestSent.postValue(DataRequestState.Error(err)) + newConfirmRequestSent.postValue(DataRequestState.Error( + Exception("Failed to accept contact request from ${contactWrapper.getUsernameFact()}") + )) }.subscribe() ) } @@ -264,24 +251,22 @@ class ContactsViewModel @Inject constructor( Timber.e("Request is still open") updateContactStatus( contactWrapper.getId(), - RequestStatus.SENT + SENT ) } err.localizedMessage?.contains("timed out") == true -> { Timber.e("Request timed out!") updateContactStatus( contactWrapper.getId(), - RequestStatus.SEND_FAIL + SEND_FAIL ) - roundRequestFail(contactWrapper) } err.localizedMessage?.contains("Cannot request authenticated channel") == true -> { Timber.e("Request failed!") updateContactStatus( contactWrapper.getId(), - RequestStatus.SEND_FAIL + SEND_FAIL ) - roundRequestFail(contactWrapper) } err.localizedMessage?.contains("Request is still open") == true -> { @@ -290,24 +275,6 @@ class ContactsViewModel @Inject constructor( } } - private fun roundRequestFail(contactWrapper: ContactWrapperBase) { - val contactRequest = preferences.getContactRequest(contactWrapper.getId()) - if (contactRequest != null) { - contactRequest.verifyState = ContactRequestState.FAILED - preferences.updateContactRequest(contactRequest) - } else { - val newContactRequest = ContactRoundRequest( - contactId = contactWrapper.getId(), - contactUsername = contactWrapper.getUsernameFact(), - roundId = -1, - isSent = true, - verifyState = ContactRequestState.FAILED - ) - Timber.e("Round request not found for ${contactWrapper.getId().toBase64String()}!") - preferences.addContactRequest(newContactRequest) - } - } - private fun newRequest(marshalledData: ByteArray) { Timber.v("[RECEIVED REQUEST] Confirming contact...") val newContact = generateContact(marshalledData) @@ -326,8 +293,8 @@ class ContactsViewModel @Inject constructor( Timber.v("Username is $contactUsername") Timber.v("Getting name...") val contactName = unmarshalledContact.getNameFact() ?: contactUsername - val contactEmail = unmarshalledContact.getEmailFact() ?: "" - val contactPhone = unmarshalledContact.getPhoneFact() ?: "" + val contactEmail = unmarshalledContact.getEmailFact(true) ?: "" + val contactPhone = unmarshalledContact.getPhoneFact(true) ?: "" Timber.v("Name is $contactName") val contact = ContactData( @@ -337,7 +304,7 @@ class ContactsViewModel @Inject constructor( marshaled = marshalledData, email = contactEmail, phone = contactPhone, - status = RequestStatus.UNVERIFIED.value + status = VERIFYING.value ) subscriptions.add( @@ -350,9 +317,7 @@ class ContactsViewModel @Inject constructor( Timber.v("Couldn't save contact: already exists") }, onSuccess = { - addRequestCount() Timber.v("${contact.userId.toBase64String()} has sent a new contact request!") - newIncomingRequestReceived.postValue(SimpleRequestState.Success(marshalledData)) verifyNewRequest(contact) }) ) @@ -360,27 +325,51 @@ class ContactsViewModel @Inject constructor( return contact } + private fun saveRequest(contact: ContactData, unread: Boolean = false) { + val contactRequest = ContactRequestData(contact, unread) + requestsDataSource.save(contactRequest) + } + fun confirmRequest(marshalledContact: ByteArray) { - val contact = repo.unmarshallContact(marshalledContact) - val bindingsId = contact!!.getId() - Timber.v("Trying to update the contact $bindingsId...") - preferences.removeContactRequests(contact.getId()) - updateContactStatus(bindingsId, RequestStatus.ACCEPTED) + viewModelScope.launch { + val contactBindings = repo.unmarshallContact(marshalledContact) + contactBindings?.let { + Timber.v("Trying to update the contact ${contactBindings.getId()}...") + getContact(contactBindings.getId())?.let { + updateContactStatus(it.userId, ACCEPTED) { + deleteRequest(it) + showRequestAccepted(it) + } + } + } + } } - private fun addReceivedContactRequest( - contact: ContactWrapperBase - ) { - val contactRoundRequest = ContactRoundRequest( - contactId = contact.getId(), - contactUsername = contact.getUsernameFact(), - roundId = -1, - isSent = false, - verifyState = ContactRequestState.RECEIVED + private suspend fun getContact(userId: ByteArray): Contact? = + daoRepo.getContactByUserId(userId).value() + + private fun showRequestAccepted(contact: Contact) { + val requestAccepted = ToastUI.create( + header = contact.displayName, + body = "Accepted your request", + leftIcon = R.drawable.ic_check, + iconTint = R.color.accent_success, + actionText = "Send message", + actionClick = { navigateToChat(contact) } ) + _showToast.value = requestAccepted + } + + fun onToastShown() { + _showToast.value = null + } + + private fun navigateToChat(contact: Contact) { + _navigateToChat.value = contact + } - preferences.removeContactRequests(contact.getId()) - preferences.addContactRequest(contactRoundRequest) + fun onNavigateHandled() { + _navigateToChat.value = null } private fun getBindingsContactId(data: ByteArray): ByteArray { @@ -392,8 +381,6 @@ class ContactsViewModel @Inject constructor( fun verifyNewRequest( contact: ContactData ) { - updateContactStatus(contact.userId, RequestStatus.VERIFYING) - addVerifyingContactRequest(contact) Timber.v("[RECEIVED REQUEST] Verifying Request ${contact.userId.toBase64String()}...") if (contact.hasFacts()) { //UD Search verifyContactViaSearch(contact) @@ -402,19 +389,6 @@ class ContactsViewModel @Inject constructor( } } - private fun addVerifyingContactRequest(contact: ContactData) { - val contactRoundRequest = ContactRoundRequest( - contactId = contact.userId, - contactUsername = contact.username, - roundId = -1, - isSent = false, - verifyState = ContactRequestState.VERIFYING - ) - - preferences.removeContactRequests(contact.userId) - preferences.addContactRequest(contactRoundRequest) - } - private fun verifyContactViaSearch(contact: ContactData) { Timber.v("[RECEIVED REQUEST] User have facts - UD Search") val factPair: Pair<String, FactType> = when { @@ -442,11 +416,14 @@ class ContactsViewModel @Inject constructor( private fun verifyContactViaLookup(contact: ContactData) { Timber.v("[RECEIVED REQUEST] User does not have facts - UD Lookup") repo.userLookup(contact.userId) { newContact, error -> - if (newContact == null) { //Fraudulent + if (!error.isNullOrEmpty()) { Timber.v("[RECEIVED REQUEST] Contact ${contact.userId.toBase64String()} is UNVERIFIED") - onContactUnverified(contact) - } else { - if (newContact.getId().contentEquals(contact.userId)) { //Verified + onFailedToVerify(contact) + return@userLookup + } + + if (newContact != null) { + if (newContact.getId().contentEquals(contact.userId)) { // Verifying Timber.v("[RECEIVED REQUEST] Contact ${contact.userId.toBase64String()} is VERIFIED") verifyContact(contact, newContact) } else { //Fraudulent @@ -466,42 +443,49 @@ class ContactsViewModel @Inject constructor( } private fun onContactVerified(contact: ContactData) { - updateContactStatus(contact.userId, RequestStatus.RECEIVED) { - addReceivedContactRequest(ContactWrapperBase.from(contact)) - Timber.v("${contact.userId.toBase64String()} has sent a new contact request!") - } + saveContact(contact, VERIFIED) + saveRequest(contact, true) + Timber.v("${contact.userId.toBase64String()} has sent a new contact request!") + newIncomingRequestReceived.postValue(SimpleRequestState.Success(contact.marshaled)) } - private fun onContactUnverified(contact: ContactData) { - updateContactStatus(contact.userId, RequestStatus.UNVERIFIED) + private fun onFailedToVerify(contact: ContactData) { + saveContact(contact, VERIFICATION_FAIL) } private fun deleteFraudulentContact(contact: ContactData) { - viewSingleRequest() + updateToDeleting(contact) + subscriptions.add( daoRepo.deleteContact(contact) .subscribeOn(schedulers.io) .observeOn(schedulers.io) .doOnError { Timber.v("[RECEIVED REQUEST] Error deleting Fraudulent contact ${contact.userId.toBase64String()}") } - .doOnSuccess { Timber.v("[RECEIVED REQUEST] Fraudulent contact has been deleted!") } + .doOnSuccess { + Timber.v("[RECEIVED REQUEST] Fraudulent contact has been deleted!") + deleteRequest(contact) + } .subscribe() ) } - fun rejectContact(contactId: ByteArray) { - deleteContact(contactId) + private fun deleteRequest(contact: Contact) { + val contactRequest = ContactRequestData(contact, true) + requestsDataSource.delete(contactRequest) } - private fun deleteContact(contactId: ByteArray) { - val request = preferences.getContactRequest(contactId) - request?.let { preferences.removeContactRequest(it) } + private fun updateToDeleting(contact: ContactData) { + updateContactStatus(contact.userId, DELETING) + } - subscriptions.add( - daoRepo.deleteContactFromDb(contactId) - .subscribeOn(schedulers.io) - .observeOn(schedulers.io) - .subscribe() - ) + fun rejectContact(contact: ContactData) { + hideRequest(contact) + } + + private fun hideRequest(contact: ContactData) { + updateContactStatus(contact.userId, HIDDEN) { + requestsDataSource.reject(ContactRequestData(contact)) + } } private fun updateContactStatus( @@ -542,68 +526,6 @@ class ContactsViewModel @Inject constructor( ) } - - private fun waitForRoundCompletion(contactId: ByteArray, searchRoundId: Long) { - Timber.v("Waiting for round $searchRoundId | contact ${contactId.toBase64String()}...") - subscriptions.add( - repo.waitForRoundCompletion( - searchRoundId, - onRoundCompletionCallback = { roundId, isSuccessful, timedOut -> - Timber.v("RoundCompletionCallback ($roundId): successful $isSuccessful timedOut $timedOut") - val request = preferences.getContactRequest(contactId, roundId) - if (request != null) { - if (isSuccessful) completeRound(request) - else failRound(request) - } - }, - timeoutMillis = 15000 - ) - .subscribeOn(schedulers.io) - .subscribe() - ) - } - - private fun completeRound(roundRequest: ContactRoundRequest) { - Timber.v("Contact round request tracker for ${roundRequest.contactId.toBase64String()} was removed") - roundRequest.verifyState = ContactRequestState.SUCCESS - preferences.updateContactRequest(roundRequest) - if (roundRequest.isSent) { - updateContactStatus(roundRequest.contactId, RequestStatus.SENT) - } else { - updateContactStatus(roundRequest.contactId, RequestStatus.ACCEPTED) - } - } - - private fun failRound(roundRequest: ContactRoundRequest) { - roundRequest.verifyState = ContactRequestState.FAILED - preferences.updateContactRequest(roundRequest) - Timber.v("Contact round request tracker for ${roundRequest.contactId.toBase64String()} was updated") - - if (roundRequest.isSent) { - updateContactStatus(roundRequest.contactId, RequestStatus.SEND_FAIL) - } else { - updateContactStatus(roundRequest.contactId, RequestStatus.CONFIRM_FAIL) - } - } - - private fun checkWaitForRounds() { - val requests = ContactRoundRequest.toRoundRequestsSet(preferences.contactRoundRequests) - for (contactRequest in requests) { - Timber.v("Round request waiting: $contactRequest") - contactRequest.apply { - when (verifyState) { - ContactRequestState.SUCCESS -> { - preferences.removeContactRequest(this) - } - ContactRequestState.VERIFYING -> { - updateContactStatus(contactRequest.contactId, RequestStatus.UNVERIFIED) - requests.remove(this) - } - } - } - } - } - private fun isAuthCallbackRegistered(): Boolean { return XxMessengerApplication.isAuthCallbackRegistered } @@ -617,15 +539,6 @@ class ContactsViewModel @Inject constructor( super.onCleared() } - fun updateContactName(temporaryContact: ContactData) { - subscriptions.add( - daoRepo.updateContactName(temporaryContact) - .subscribeOn(schedulers.io) - .observeOn(schedulers.main) - .subscribe() - ) - } - fun createGroup(name: String, initialMsg: String?, contactsList: List<ContactData>) { newGroupRequestSent.value = DataRequestState.Start() val idsList = contactsList.map { contacts -> @@ -706,7 +619,9 @@ class ContactsViewModel @Inject constructor( .observeOn(schedulers.main) .doOnError { err -> Timber.e("[GROUP ACCEPT] Error on accepting group: ${err.localizedMessage}") - acceptGroupRequest.value = DataRequestState.Error(err) + acceptGroupRequest.postValue(DataRequestState.Error( + Exception("Failed to join group ${group.name}") + )) } .doOnSuccess { Timber.v("[GROUP ACCEPT] Finished with success!") diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/EmailRegistration.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/EmailRegistration.kt index 8e4adda3487b1ebd626274f828fbba64d4ee3bc7..1602ceee06faff54b314191cd78ad5da3993bd7c 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/EmailRegistration.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/EmailRegistration.kt @@ -14,8 +14,8 @@ import io.xxlabs.messenger.application.SchedulerProvider import io.xxlabs.messenger.bindings.wrapper.bindings.bindingsErrorMessage import io.xxlabs.messenger.data.datatype.FactType import io.xxlabs.messenger.repository.base.BaseRepository -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI -import io.xxlabs.messenger.support.dialog.info.SpanConfig +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.SpanConfig import io.xxlabs.messenger.ui.intro.registration.tfa.TwoFactorAuthCredentials import javax.inject.Inject diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/EmailRegistrationUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/EmailRegistrationUI.kt index 84790baf3b4c307b7d18c3e69c06535dbdcd2f00..933f29abef1e80f1b54cc4c08d7e0b70142b11dc 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/EmailRegistrationUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/EmailRegistrationUI.kt @@ -3,7 +3,7 @@ package io.xxlabs.messenger.ui.intro.registration.email import android.text.Spanned import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI import io.xxlabs.messenger.ui.intro.registration.tfa.TwoFactorAuthCredentials interface EmailRegistrationUI { diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/RegistrationEmailFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/RegistrationEmailFragment.kt index 2b14800fd10ba1972c8c750499d988a452098fd2..ac452d37e905808619bf6575561eeaecac4cb568 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/RegistrationEmailFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/email/RegistrationEmailFragment.kt @@ -9,7 +9,7 @@ import androidx.navigation.fragment.findNavController import io.xxlabs.messenger.R import io.xxlabs.messenger.databinding.FragmentRegistrationEmailBinding import io.xxlabs.messenger.di.utils.Injectable -import io.xxlabs.messenger.support.dialog.info.InfoDialog +import io.xxlabs.messenger.ui.dialog.info.InfoDialog import io.xxlabs.messenger.ui.intro.registration.tfa.TwoFactorAuthCredentials import io.xxlabs.messenger.ui.intro.registration.RegistrationFlowFragment diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/PhoneRegistration.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/PhoneRegistration.kt index b3111993ba0680da988fb508fe5f2146b6cc1494..6dc41a0d052c29a8ab592e7b60e31f9a47673a05 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/PhoneRegistration.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/PhoneRegistration.kt @@ -16,8 +16,8 @@ import io.xxlabs.messenger.bindings.wrapper.bindings.bindingsErrorMessage import io.xxlabs.messenger.data.data.Country import io.xxlabs.messenger.data.datatype.FactType import io.xxlabs.messenger.repository.base.BaseRepository -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI -import io.xxlabs.messenger.support.dialog.info.SpanConfig +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.SpanConfig import io.xxlabs.messenger.ui.intro.registration.tfa.TwoFactorAuthCredentials import javax.inject.Inject diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/PhoneRegistrationUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/PhoneRegistrationUI.kt index 99c112c88b5cc900d37d39ceda4eb39b5a408904..92edba821943ca61087deb0045d3e764469dcc46 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/PhoneRegistrationUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/PhoneRegistrationUI.kt @@ -4,7 +4,7 @@ import android.text.Spanned import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import io.xxlabs.messenger.data.data.Country -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI import io.xxlabs.messenger.ui.intro.registration.tfa.TwoFactorAuthCredentials interface PhoneRegistrationUI { diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/RegistrationPhoneFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/RegistrationPhoneFragment.kt index bfab7dc929b44da9e815d589a364a59dce6fd606..e307a437e08ced4cee15da9fd593ce773df6943f 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/RegistrationPhoneFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/phone/RegistrationPhoneFragment.kt @@ -10,7 +10,7 @@ import io.xxlabs.messenger.R import io.xxlabs.messenger.data.data.Country import io.xxlabs.messenger.databinding.FragmentRegistrationPhoneBinding import io.xxlabs.messenger.di.utils.Injectable -import io.xxlabs.messenger.support.dialog.info.InfoDialog +import io.xxlabs.messenger.ui.dialog.info.InfoDialog import io.xxlabs.messenger.ui.intro.registration.tfa.TwoFactorAuthCredentials import io.xxlabs.messenger.ui.intro.registration.RegistrationFlowFragment import io.xxlabs.messenger.ui.main.countrycode.CountryFullscreenDialog diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/success/RegistrationCompletedStepFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/success/RegistrationCompletedStepFragment.kt index 04be50a0bf557555b3abb03b05e4abb9ab866020..638aa45ae392ddf10c30c5f54d6e69c990f46b82 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/success/RegistrationCompletedStepFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/success/RegistrationCompletedStepFragment.kt @@ -57,7 +57,7 @@ class RegistrationCompletedStepFragment : RegistrationFlowFragment(), Injectable if (it) { when (step) { EMAIL -> navigateNextStep() - PHONE -> navigateToChats() + PHONE -> registrationComplete() RESTORE -> restartApp() } } @@ -71,7 +71,7 @@ class RegistrationCompletedStepFragment : RegistrationFlowFragment(), Injectable ui.onCompletedStepNavigateHandled(step) } - private fun navigateToChats() { + private fun registrationComplete() { onRegistrationComplete() ui.onCompletedStepNavigateHandled(step) } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/RegistrationTfaFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/RegistrationTfaFragment.kt index f8f070d2815b0cfce07b3d8c7cfee1461780dade..0e49bb86fdcc4faf94c88e0a99c4f62b77940b9c 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/RegistrationTfaFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/RegistrationTfaFragment.kt @@ -10,7 +10,7 @@ import io.xxlabs.messenger.R import io.xxlabs.messenger.data.datatype.FactType import io.xxlabs.messenger.databinding.FragmentRegistration2faBinding import io.xxlabs.messenger.di.utils.Injectable -import io.xxlabs.messenger.support.dialog.info.InfoDialog +import io.xxlabs.messenger.ui.dialog.info.InfoDialog import io.xxlabs.messenger.ui.intro.registration.success.RegistrationStep import io.xxlabs.messenger.ui.intro.registration.RegistrationFlowFragment diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/TfaRegistration.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/TfaRegistration.kt index 0f81c923ef35ffe8fdd553450448b10a1cd1b977..0754bcc73599c7dc3b50a5ba05c72245012fc2f4 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/TfaRegistration.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/TfaRegistration.kt @@ -8,7 +8,7 @@ import io.xxlabs.messenger.R import io.xxlabs.messenger.application.SchedulerProvider import io.xxlabs.messenger.data.datatype.FactType import io.xxlabs.messenger.repository.base.BaseRepository -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI import javax.inject.Inject class TfaRegistration @Inject constructor( diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/TfaRegistrationUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/TfaRegistrationUI.kt index 7d182ee4ad0a19bdb4fbefe5f6923607e3c6e82f..ee6374afc517203100f0052bc33c3afb5fc326c3 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/TfaRegistrationUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/tfa/TfaRegistrationUI.kt @@ -2,7 +2,7 @@ package io.xxlabs.messenger.ui.intro.registration.tfa import android.text.Spanned import androidx.lifecycle.LiveData -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI interface TfaRegistrationUI { val maxTfaInputLength: Int diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/RegistrationUsernameFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/RegistrationUsernameFragment.kt index 105de1eaafaf388951edfae039fa22082f6fbcdb..aee2ea96ba7e96ead1defb840be2bd78382d0a38 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/RegistrationUsernameFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/RegistrationUsernameFragment.kt @@ -8,7 +8,7 @@ import androidx.databinding.DataBindingUtil import androidx.navigation.fragment.findNavController import io.xxlabs.messenger.R import io.xxlabs.messenger.databinding.FragmentRegistrationUsernameBinding -import io.xxlabs.messenger.support.dialog.info.InfoDialog +import io.xxlabs.messenger.ui.dialog.info.InfoDialog import io.xxlabs.messenger.ui.intro.registration.RegistrationFlowFragment /** diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistration.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistration.kt index 82f7bb31994876b15c886193d0bb1b5ec607a01f..be8f9f61d42d55577297990e1a7243b26baafcd3 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistration.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistration.kt @@ -19,8 +19,8 @@ import io.xxlabs.messenger.bindings.wrapper.bindings.bindingsErrorMessage import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.repository.base.BaseRepository import io.xxlabs.messenger.support.appContext -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI -import io.xxlabs.messenger.support.dialog.info.SpanConfig +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.SpanConfig import io.xxlabs.messenger.ui.global.NetworkViewModel import kotlinx.coroutines.* import kotlin.random.Random.Default.nextInt @@ -104,11 +104,13 @@ class UsernameRegistration @AssistedInject constructor( override val usernameNavigateRestore: LiveData<Boolean> get() = navigateRestore private val navigateRestore = MutableLiveData(false) - override val usernameFilters: Array<InputFilter> get() = + override val usernameFilters: Array<InputFilter> = arrayOf( InputFilter { source, start, end, _, _, _ -> - source?.subSequence(start, end) + val input = source?.subSequence(start, end) + val filtered = source?.subSequence(start, end) ?.replace(Regex(USERNAME_FILTER_REGEX), "") + if (filtered == input) null else filtered } ) @@ -134,7 +136,7 @@ class UsernameRegistration @AssistedInject constructor( override fun onUsernameInput(text: Editable) { error.postValue(null) - username.value = text.toString() + username.postValue(text.toString()) } private fun disableUI() { diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistrationUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistrationUI.kt index e50d49cc6248a067baef4483e453a2d529a16997..d0794dffe7e4959e24161b7c1f44775757589f17 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistrationUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistrationUI.kt @@ -4,8 +4,7 @@ import android.text.Editable import android.text.InputFilter import android.text.Spanned import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI interface UsernameRegistrationUI { val usernameTitle: Spanned diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/RegistrationWelcomeFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/RegistrationWelcomeFragment.kt index 701243e439109e70e45da90192e9aa2a9980c8d8..66a1fe49994738eef78baf6f6ba164014a6a4fc4 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/RegistrationWelcomeFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/RegistrationWelcomeFragment.kt @@ -8,7 +8,7 @@ import androidx.databinding.DataBindingUtil import androidx.navigation.fragment.findNavController import io.xxlabs.messenger.R import io.xxlabs.messenger.databinding.FragmentRegistrationWelcomeBinding -import io.xxlabs.messenger.support.dialog.info.InfoDialog +import io.xxlabs.messenger.ui.dialog.info.InfoDialog import io.xxlabs.messenger.ui.intro.registration.RegistrationFlowFragment class RegistrationWelcomeFragment : RegistrationFlowFragment() { diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/WelcomeRegistration.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/WelcomeRegistration.kt index 37c939e88756bbf2c6d83f5dcefd30b0d24a272e..7d08a2ae24541bd19fbef7ad85fc0e5cc19dadf5 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/WelcomeRegistration.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/WelcomeRegistration.kt @@ -8,8 +8,8 @@ import android.text.style.ForegroundColorSpan import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import io.xxlabs.messenger.R -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI -import io.xxlabs.messenger.support.dialog.info.SpanConfig +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.SpanConfig import javax.inject.Inject class WelcomeRegistration @Inject constructor( diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/WelcomeRegistrationUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/WelcomeRegistrationUI.kt index 8092e9f55d9527d82700b10d912073c963c4272d..6300f3784a9c8e39a9b9a6514feeaca59da85afe 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/WelcomeRegistrationUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/welcome/WelcomeRegistrationUI.kt @@ -2,7 +2,7 @@ package io.xxlabs.messenger.ui.intro.registration.welcome import android.text.Spanned import androidx.lifecycle.LiveData -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI interface WelcomeRegistrationUI { fun welcomeTitle(username: String): Spanned diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt index 2b56dbdc57d5fffb429d5633c224ccef77177c45..cf8406474c8d278657080fde45a51facb5f68f9b 100755 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt @@ -6,33 +6,41 @@ import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context import android.graphics.BitmapFactory +import android.graphics.Color import android.os.Bundle -import android.view.MenuItem -import android.view.MotionEvent -import android.view.View -import android.view.WindowManager +import android.os.PersistableBundle +import android.view.* import android.view.inputmethod.InputMethodManager +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.os.bundleOf import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider +import androidx.core.view.setPadding +import androidx.lifecycle.* import androidx.navigation.NavController import androidx.navigation.Navigation +import androidx.navigation.findNavController import androidx.navigation.ui.onNavDestinationSelected import com.bumptech.glide.Glide import com.google.android.material.shape.CornerFamily +import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import io.xxlabs.messenger.BuildConfig import io.xxlabs.messenger.R import io.xxlabs.messenger.bindings.wrapper.bindings.BindingsWrapperBindings import io.xxlabs.messenger.data.data.DataRequestState import io.xxlabs.messenger.data.data.SimpleRequestState +import io.xxlabs.messenger.data.room.model.Contact +import io.xxlabs.messenger.databinding.ComponentCustomToastBinding import io.xxlabs.messenger.media.MediaProviderActivity import io.xxlabs.messenger.notifications.MessagingService +import io.xxlabs.messenger.support.appContext import io.xxlabs.messenger.support.callback.NetworkWatcher import io.xxlabs.messenger.support.dialog.PopupActionBottomDialog import io.xxlabs.messenger.support.extensions.* import io.xxlabs.messenger.support.isMockVersion import io.xxlabs.messenger.support.misc.DebugLogger +import io.xxlabs.messenger.support.toast.CustomToastActivity +import io.xxlabs.messenger.support.toast.ToastUI import io.xxlabs.messenger.support.util.DialogUtils import io.xxlabs.messenger.support.util.Utils import io.xxlabs.messenger.support.view.LooperCircularProgressBar @@ -46,10 +54,13 @@ import io.xxlabs.messenger.ui.main.chats.ChatsViewModel import io.xxlabs.messenger.ui.main.contacts.PhotoSelectorFragment import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.component_menu.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -class MainActivity : MediaProviderActivity(), SnackBarActivity { +class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActivity { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -66,6 +77,7 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity { override fun onStart() { super.onStart() + observeUI() watchObservables() mainViewModel.checkIsLoggedInReturn() } @@ -283,6 +295,39 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity { } } + private fun observeUI() { + contactsViewModel.showToast.onEach { toast -> + toast?.let { + showCustomToast(toast) + contactsViewModel.onToastShown() + } + }.launchIn(lifecycleScope) + + contactsViewModel.navigateToChat.onEach { contact -> + contact?.let { + openChat(contact) + contactsViewModel.onNavigateHandled() + } + }.launchIn(lifecycleScope) + } + + + private fun openChat(contact: Contact) { + val bundle = bundleOf("contact_id" to contact.userId) + mainNavController.navigateSafe(R.id.action_global_chat, bundle) + } + + private fun requestFailedToast(message: String?) { + showCustomToast( + ToastUI.create( + body = message ?: "One of your requests failed.", + leftIcon = R.drawable.ic_danger, + backgroundColor = getColor(R.color.accent_danger) + ) + ) + contactsViewModel.onToastShown() + } + private fun watchObservables() { mainViewModel.loginStatus.observe(this, Observer { result -> Timber.v("[LOGIN] Status - Main") @@ -362,8 +407,7 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity { contactsViewModel.newAuthRequestSent.postValue(SimpleRequestState.Completed()) } is SimpleRequestState.Error -> { - createSnackMessage("One of your requests has failed!") -// contactsViewModel.addRequestCount() + requestFailedToast(result.error?.message) } else -> { Timber.v("Completed new auth request") @@ -375,8 +419,7 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity { Timber.v("New confirm request - UI - $result") when (result) { is DataRequestState.Error -> { - createSnackMessage("One of your requests has failed!") -// contactsViewModel.addRequestCount() + requestFailedToast(result.error.message) } is DataRequestState.Success -> { @@ -402,7 +445,6 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity { if (result is SimpleRequestState.Success) { Timber.v("Request is success") createSnackMessage("A contact has accepted your private channel request!") - contactsViewModel.viewSingleRequest() contactsViewModel.newConfirmationRequestReceived.postValue(SimpleRequestState.Completed()) } else { Timber.v("Completed confirm contact post") @@ -426,7 +468,6 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity { if (result is SimpleRequestState.Success) { Timber.v("Request is success") createSnackMessage("Private Group invitation received!") - contactsViewModel.addRequestCount() mainViewModel.newGroup.postValue(SimpleRequestState.Completed()) } else { Timber.v("Completed confirm contact post") @@ -648,4 +689,21 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity { return activeInstances > 0 } } + + override fun showCustomToast(ui: ToastUI) { + val snackBar = Snackbar.make(findViewById(R.id.customToastView), "", ui.duration) + val binding = ComponentCustomToastBinding.inflate(layoutInflater) + binding.ui = ui + (snackBar.view as Snackbar.SnackbarLayout).apply { + setBackgroundColor(Color.TRANSPARENT) + setPadding(48) + addView(binding.root, 0) + + val params = layoutParams as CoordinatorLayout.LayoutParams + params.gravity = Gravity.TOP + layoutParams = params + } + snackBar.animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE + snackBar.show() + } } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/MainViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/MainViewModel.kt index 682806af1d8c4f2ab1e6f3a03e2342610d7f93f3..52cfd7bd4eb42c35d195a64402e3fc7c7cc5046d 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/MainViewModel.kt @@ -3,6 +3,7 @@ package io.xxlabs.messenger.ui.main import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.reactivex.Single import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.subscribeBy @@ -17,22 +18,29 @@ import io.xxlabs.messenger.data.data.SimpleRequestState import io.xxlabs.messenger.data.datatype.MessageStatus import io.xxlabs.messenger.data.datatype.RequestStatus import io.xxlabs.messenger.data.room.model.ChatMessage +import io.xxlabs.messenger.data.room.model.GroupData import io.xxlabs.messenger.data.room.model.GroupMessageData import io.xxlabs.messenger.repository.DaoRepository import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.repository.base.BaseRepository +import io.xxlabs.messenger.requests.data.group.GroupInvitationData +import io.xxlabs.messenger.requests.data.group.GroupRequestsRepository +import io.xxlabs.messenger.requests.data.group.InvitationMigrator +import io.xxlabs.messenger.requests.model.GroupInvitation import io.xxlabs.messenger.support.extensions.toBase64String import io.xxlabs.messenger.support.isMockVersion import io.xxlabs.messenger.support.util.Utils +import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import javax.inject.Inject class MainViewModel @Inject constructor( - val repo: BaseRepository, - val daoRepo: DaoRepository, - val preferences: PreferencesRepository, - val schedulers: SchedulerProvider + private val repo: BaseRepository, + private val daoRepo: DaoRepository, + private val preferences: PreferencesRepository, + private val schedulers: SchedulerProvider, + private val invitationsDataSource: GroupRequestsRepository ) : ViewModel() { var isMenuOpened = MutableLiveData<Boolean>() var subscriptions = CompositeDisposable() @@ -49,6 +57,16 @@ class MainViewModel @Inject constructor( @Volatile var wasLoggedIn = false + init { + migrateOldInvitations() + } + + private fun migrateOldInvitations() { + viewModelScope.launch { + InvitationMigrator.performMigration(invitationsDataSource, daoRepo) + } + } + fun checkIsLoggedInReturn() { subscriptions.add( repo.isLoggedIn() @@ -223,7 +241,8 @@ class MainViewModel @Inject constructor( "[GROUP REQUEST] Group name is ${group.getName().decodeToString()}" ) - subscriptions.add(daoRepo.createUserGroup(group, RequestStatus.RECEIVED) + val status = RequestStatus.VERIFIED + subscriptions.add(daoRepo.createUserGroup(group, status) .flatMap { daoRepo.insertGroupMemberShip( group.getID(), @@ -261,6 +280,12 @@ class MainViewModel @Inject constructor( .doOnError { err -> Timber.e(err) } .doOnSuccess { newGroup.value = SimpleRequestState.Success(it) + invitationsDataSource.save( + GroupInvitationData( + model = GroupData.from(group, status), + unread = true + ) + ) } .subscribe()) } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/chat/DataBindingUtils.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/chat/DataBindingUtils.kt index 430a650715ec49beaac5b97d2fee29a5e0f602a7..a7be6a1a48de029c8fd9d5fd97d0ccfd70a3c502 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/chat/DataBindingUtils.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/chat/DataBindingUtils.kt @@ -10,7 +10,11 @@ import android.widget.EditText import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes import androidx.core.content.ContextCompat +import androidx.core.view.setPadding import androidx.databinding.BindingAdapter import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -20,9 +24,11 @@ import io.xxlabs.messenger.backup.model.BackupProgress import io.xxlabs.messenger.support.extensions.disableWithAlpha import io.xxlabs.messenger.support.extensions.enable import io.xxlabs.messenger.support.extensions.incognito +import io.xxlabs.messenger.support.extensions.setTint import java.text.DateFormat import java.util.* + @BindingAdapter("android:visibility") fun View.setVisibility(visible: Boolean) { visibility = if (visible) View.VISIBLE else View.GONE @@ -157,4 +163,69 @@ fun TextView.setProgress(task: BackupProgress?) { else -> "Restore in progress" } } +} + +@BindingAdapter("backgroundTint") +fun View.backgroundTint(color: Int?) { + color?.let { background.setTint(it) } +} + +@BindingAdapter("actionIcon", "iconPosition", "iconColor", requireAll = true) +fun TextView.addDrawable( + @DrawableRes iconRes: Int?, + position: DrawablePosition, + @ColorRes colorRes: Int? +) { + iconRes?.let { + when (position) { + DrawablePosition.TOP -> setCompoundDrawablesWithIntrinsicBounds(0, iconRes, 0, 0) + DrawablePosition.BOTTOM -> setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, iconRes) + DrawablePosition.START -> setCompoundDrawablesWithIntrinsicBounds(iconRes, 0, 0, 0) + DrawablePosition.END -> setCompoundDrawablesWithIntrinsicBounds(0, 0, iconRes, 0) + } + compoundDrawablePadding = 6 + for (drawable in compoundDrawables) { + drawable?.setTint(resources.getColor(colorRes ?: currentTextColor, null)) + } + } +} + +enum class DrawablePosition { + TOP, START, END, BOTTOM +} + +@BindingAdapter("thumbnailBitmap", "thumbnailIcon", requireAll = true) +fun ImageView.thumbnail(bitmap: Bitmap?, @IdRes icon: Int?) { + bitmap?.let { + visibility = View.VISIBLE + setPadding(0) + Glide.with(context) + .asBitmap() + .diskCacheStrategy(DiskCacheStrategy.DATA) + .apply(RequestOptions().override(50, 50)) + .centerCrop() + .load(it) + .into(this) + } ?: icon?.let { + setImageResource(it) + } ?: run { + Glide.with(context).clear(this) + } +} + +@BindingAdapter("actionIcon") +fun ImageView.icon(@IdRes icon: Int?) { + icon?.let { setImageResource(it) } +} + +@BindingAdapter("actionIconTint") +fun ImageView.actionIconTint(@ColorRes color: Int?) { + color?.let { setTint(it) } +} + +@BindingAdapter("customStyle") +fun TextView.setStyle(@IdRes id: Int?) { + id?.let { + setTextAppearance(it) + } } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/chat/PrivateMessagesFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/chat/PrivateMessagesFragment.kt index dcd5de58d9855810c6052dc889afbf89cfc94601..8faf54b532861b1ae9aa6b968eeb0a22590dbda8 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/chat/PrivateMessagesFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/chat/PrivateMessagesFragment.kt @@ -20,6 +20,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import io.xxlabs.messenger.BuildConfig import io.xxlabs.messenger.R import io.xxlabs.messenger.data.room.model.ChatMessage +import io.xxlabs.messenger.data.room.model.ContactData import io.xxlabs.messenger.data.room.model.PrivateMessage import io.xxlabs.messenger.media.* import io.xxlabs.messenger.support.dialog.BottomSheetPopup @@ -32,6 +33,7 @@ import io.xxlabs.messenger.ui.main.chat.adapters.AttachmentListener import io.xxlabs.messenger.ui.main.chat.adapters.AttachmentsAdapter import io.xxlabs.messenger.ui.main.chat.adapters.ChatMessagesAdapter import io.xxlabs.messenger.ui.main.chat.adapters.PrivateMessagesAdapter +import io.xxlabs.messenger.ui.dialog.warning.showConfirmDialog import timber.log.Timber import java.io.File import java.io.IOException @@ -46,7 +48,16 @@ class PrivateMessagesFragment : { private val contactId: ByteArray by lazy { - requireArguments().getByteArray("contact_id")!! + PrivateMessagesFragmentArgs + .fromBundle(requireArguments()) + .contactId + ?.encodeToByteArray() + ?: requireArguments().getByteArray("contact_id") !! + } + private val cachedContact: ContactData? by lazy { + PrivateMessagesFragmentArgs + .fromBundle(requireArguments()) + .contact } /* ViewModels */ @@ -57,7 +68,8 @@ class PrivateMessagesFragment : private val chatViewModel: PrivateMessagesViewModel by viewModels { PrivateMessagesViewModel.provideFactory( chatViewModelFactory, - contactId + contactId, + cachedContact ) } override val uiController: ChatMessagesUIController<PrivateMessage> diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/chat/PrivateMessagesViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/chat/PrivateMessagesViewModel.kt index 1e56337fa30d4463120663031967da8a835a372b..69a8f73b900d55647142e78c35a7bac974c40a6d 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/chat/PrivateMessagesViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/chat/PrivateMessagesViewModel.kt @@ -49,7 +49,8 @@ class PrivateMessagesViewModel @AssistedInject constructor( schedulers: SchedulerProvider, preferences: PreferencesRepository, application: Application, - @Assisted private val contactId: ByteArray + @Assisted private val contactId: ByteArray, + @Assisted private val cachedContact: ContactData? = null ) : ChatMessagesViewModel<PrivateMessage>( repo, daoRepo, schedulers, preferences, application, contactId ) { @@ -99,7 +100,13 @@ class PrivateMessagesViewModel @AssistedInject constructor( private val contactData: MutableLiveData<ContactData> = MutableLiveData() init { - startSubscription(contactId) + cachedContact?.let { + this.contact = it + contactData.value = it + getMessages(it.userId) + } ?: run { + startSubscription(contactId) + } } /* UI */ @@ -944,10 +951,11 @@ class PrivateMessagesViewModel @AssistedInject constructor( companion object { fun provideFactory( assistedFactory: PrivateMessagesViewModelFactory, - contactId: ByteArray + contactId: ByteArray, + cachedContact: ContactData? = null ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { - return assistedFactory.create(contactId) as T + return assistedFactory.create(contactId, cachedContact) as T } } } @@ -955,5 +963,5 @@ class PrivateMessagesViewModel @AssistedInject constructor( @AssistedFactory interface PrivateMessagesViewModelFactory { - fun create(contactId: ByteArray): PrivateMessagesViewModel + fun create(contactId: ByteArray, cachedContact: ContactData? = null): PrivateMessagesViewModel } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/chats/ChatsFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/chats/ChatsFragment.kt index e34427c47c226c23d5eb45ea6c9a5db49f3331bd..b8082c8a863b51ce5ae152f8e81275d4704f7bdd 100755 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/chats/ChatsFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/chats/ChatsFragment.kt @@ -36,6 +36,8 @@ import io.xxlabs.messenger.ui.base.BaseFragment import io.xxlabs.messenger.ui.global.ContactsViewModel import io.xxlabs.messenger.ui.global.NetworkViewModel import io.xxlabs.messenger.ui.main.MainViewModel +import io.xxlabs.messenger.ui.dialog.warning.showConfirmDialog +import io.xxlabs.messenger.ui.dialog.info.showTwoButtonInfoDialog import kotlinx.android.synthetic.main.component_bottom_menu_chats.* import kotlinx.android.synthetic.main.component_network_error_banner.* import kotlinx.android.synthetic.main.fragment_chats_list.* @@ -82,10 +84,12 @@ class ChatsFragment : BaseFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) navController = findNavController() + navigateForNewUsers() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + mainViewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(MainViewModel::class.java) @@ -104,6 +108,18 @@ class ChatsFragment : BaseFragment() { initComponents(view) } + private fun navigateForNewUsers() { + if (preferences.userData.isNotBlank() && preferences.isFirstLaunch) { + navigateToUdSearch() + } + } + + private fun navigateToUdSearch() { + val udSearch = ChatsFragmentDirections.actionChatsToUdSearch() + preferences.isFirstLaunch = false + findNavController().navigate(udSearch) + } + fun initComponents(root: View) { root.setInsets( bottomMask = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime(), @@ -113,7 +129,6 @@ class ChatsFragment : BaseFragment() { setListeners() bindRecyclerView() resetSearchBar() - showNotificationDialog() } override fun onStart() { @@ -121,58 +136,6 @@ class ChatsFragment : BaseFragment() { watchForObservables() } - private fun showNotificationDialog() { - if (preferences.userData.isNotBlank() && preferences.isFirstTimeNotifications) { - showTwoButtonInfoDialog( - R.string.settings_push_notifications_dialog_title, - R.string.settings_push_notifications_dialog_body, - null, - ::enablePushNotifications, - null, - ::showCoverMessageDialog - ) - preferences.isFirstTimeNotifications = false - } - } - - private fun enablePushNotifications() { - mainViewModel.enableNotifications { error -> - error?.let { showError(error) } - } - } - - private fun showCoverMessageDialog() { - if (preferences.userData.isNotBlank() && preferences.isFirstTimeCoverMessages) { - showTwoButtonInfoDialog( - R.string.settings_cover_traffic_title, - R.string.settings_cover_traffic_dialog_body, - mapOf( - getString(R.string.settings_cover_traffic_link_text) - to getString(R.string.settings_cover_traffic_link_url) - ), - ::enableCoverMessages, - ::declineCoverMessages, - ) - preferences.isFirstTimeCoverMessages = false - } - } - - private fun enableCoverMessages() { - enableDummyTraffic(true) - } - - private fun declineCoverMessages() { - enableDummyTraffic(false) - } - - private fun enableDummyTraffic(enabled: Boolean) { - try { - mainViewModel.enableDummyTraffic(enabled) - } catch (e: Exception) { - showError(e, true) - } - } - private fun setListeners() { chatsSearchBar.incognito(preferences.isIncognitoKeyboardEnabled) chatsSearchBar.addTextChangedListener { text -> diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsFragment.kt index a68103e4dc369c8a4081fbf47231afabc93934e4..75a0beb3be648585eee03065854ecb88d0729a4f 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsFragment.kt @@ -23,12 +23,12 @@ import io.xxlabs.messenger.support.dialog.PopupActionBottomDialogFragment import io.xxlabs.messenger.support.extensions.* import io.xxlabs.messenger.support.view.LooperCircularProgressBar import io.xxlabs.messenger.ui.base.BaseFragment +import io.xxlabs.messenger.ui.dialog.info.showInfoDialog import io.xxlabs.messenger.ui.global.ContactsViewModel import io.xxlabs.messenger.ui.global.NetworkViewModel import io.xxlabs.messenger.ui.main.MainActivity import io.xxlabs.messenger.ui.main.groups.create.CreateGroupDialog import io.xxlabs.messenger.ui.main.groups.create.CreateGroupDialogUI -import kotlinx.android.synthetic.main.component_menu.* import kotlinx.android.synthetic.main.component_network_error_banner.* import kotlinx.android.synthetic.main.component_toolbar_generic.* import kotlinx.android.synthetic.main.fragment_contacts.* diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsListAdapter.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsListAdapter.kt index aae2e3066ab2b40c68550ce39b4693470dd516fb..675469e57e61796d79c6343702ffcd83372ed87f 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsListAdapter.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsListAdapter.kt @@ -115,7 +115,7 @@ class ContactsListAdapter( if (isSelectionMode) { holder.setOnClick(SelectionMode.CONTACT_ACCESS) } else { - holder.setOnClick(SelectionMode.PROFILE_ACCESS) + holder.setOnClick(SelectionMode.CHAT_ACCESS) } holder.showDivider(position != 0 && position != composedItemsList.size - 1) diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsViewHolder.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsViewHolder.kt index 6e5c9abb66b7a02652e8d3749c2d9b5dc127ff99..aabcf490398f57ef34140aff2b3ad9a23a6836fb 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsViewHolder.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/ContactsViewHolder.kt @@ -49,7 +49,7 @@ class ContactsViewHolder( fun setState(friendStatus: Int) { contactUsername.alpha = if (friendStatus == RequestStatus.ACCEPTED.value - || friendStatus == RequestStatus.RECEIVED.value + || friendStatus == RequestStatus.VERIFIED.value ) { 1.0f } else { @@ -57,7 +57,7 @@ class ContactsViewHolder( } contactPhoto.alpha = if (friendStatus == RequestStatus.ACCEPTED.value - || friendStatus == RequestStatus.RECEIVED.value + || friendStatus == RequestStatus.VERIFIED.value ) { 1.0f } else { @@ -137,7 +137,7 @@ class ContactsViewHolder( } fun setContactUsernameText(text: String) { - contactUsername.text = text.capitalizeWords() + contactUsername.text = text } fun setOnClick(selectionMode: SelectionMode) { @@ -220,7 +220,7 @@ class ContactsViewHolder( } private fun navigateFromChat(bundle: Bundle) { - if (requestStatus == RequestStatus.RECEIVED) { + if (requestStatus == RequestStatus.VERIFIED) { Navigation.findNavController(parent) .navigate(R.id.action_contacts_to_invitation, bundle) } else { @@ -235,7 +235,7 @@ class ContactsViewHolder( } private fun navigateFromContacts(bundle: Bundle) { - if (requestStatus == RequestStatus.RECEIVED) { + if (requestStatus == RequestStatus.VERIFIED) { Navigation.findNavController(parent) .navigate(R.id.action_contacts_to_invitation, bundle) } else { @@ -246,7 +246,7 @@ class ContactsViewHolder( private fun navigateFromContactsSelection(bundle: Bundle) { when (requestStatus) { - RequestStatus.RECEIVED -> { + RequestStatus.VERIFIED -> { Navigation.findNavController(parent) .navigate(R.id.action_contacts_selection_to_invitation, bundle) } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/delete/DeleteConnectionDialogUI.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/delete/DeleteConnectionDialogUI.kt index 37a0859f9937a0a013fc2befe239bd947244df92..ed8468c5ba0a2cecd7a54b6afe54cb2f093742e9 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/delete/DeleteConnectionDialogUI.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/delete/DeleteConnectionDialogUI.kt @@ -1,16 +1,16 @@ package io.xxlabs.messenger.ui.main.contacts.delete -import io.xxlabs.messenger.support.dialog.confirm.ConfirmDialogUI +import io.xxlabs.messenger.ui.dialog.warning.WarningDialogUI -interface DeleteConnectionDialogUI : ConfirmDialogUI { +interface DeleteConnectionDialogUI : WarningDialogUI { val bodyClicked: () -> Unit companion object Factory { fun create( - confirmDialog: ConfirmDialogUI, + warningDialog: WarningDialogUI, bodyClicked: () -> Unit ): DeleteConnectionDialogUI { - return object : DeleteConnectionDialogUI, ConfirmDialogUI by confirmDialog { + return object : DeleteConnectionDialogUI, WarningDialogUI by warningDialog { override val bodyClicked = bodyClicked } } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/invitation/ContactInvitation.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/invitation/ContactInvitation.kt index e32f3ef3074f1bfd19349bf4694333ed7dd48e30..57eaa8e117e3c815a659a4be7f243d50d4327d65 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/invitation/ContactInvitation.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/invitation/ContactInvitation.kt @@ -71,7 +71,7 @@ class ContactInvitation : BaseContactDetailsFragment() { } contactDetailsBtnReject.setOnSingleClickListener { - contactsViewModel.rejectContact(contact.marshaled!!) + contactsViewModel.rejectContact(contact) } } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/profile/ContactProfileFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/profile/ContactProfileFragment.kt index 08e2044d87490ded199f7e5f8dfc7b3adac96acc..8db46f7230ee6fa2f9d62d192f9e27a90bcaef45 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/profile/ContactProfileFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/contacts/profile/ContactProfileFragment.kt @@ -9,10 +9,11 @@ import io.xxlabs.messenger.R import io.xxlabs.messenger.data.data.DataRequestState import io.xxlabs.messenger.data.room.model.ContactData import io.xxlabs.messenger.support.dialog.PopupActionBottomDialogFragment -import io.xxlabs.messenger.support.dialog.confirm.ConfirmDialogUI -import io.xxlabs.messenger.support.dialog.info.InfoDialogUI +import io.xxlabs.messenger.ui.dialog.warning.WarningDialogUI +import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI import io.xxlabs.messenger.support.extensions.setOnSingleClickListener import io.xxlabs.messenger.ui.base.BaseContactDetailsFragment +import io.xxlabs.messenger.ui.dialog.info.showInfoDialog import io.xxlabs.messenger.ui.main.MainActivity import io.xxlabs.messenger.ui.main.contacts.delete.DeleteConnectionDialog import io.xxlabs.messenger.ui.main.contacts.delete.DeleteConnectionDialogUI @@ -126,7 +127,7 @@ class ContactProfileFragment: BaseContactDetailsFragment() { getString(R.string.confirm_delete_connection_dialog_body, currContact.displayName), null ) { } - val confirmDialogUI = ConfirmDialogUI.create( + val confirmDialogUI = WarningDialogUI.create( infoDialogUI, getString(R.string.confirm_delete_connection_dialog_button), ::onDeleteConnection diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/groups/GroupMessagesFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/groups/GroupMessagesFragment.kt index 5431c9eb856dd032c0329d121ab167b820c13a3b..bf2c5f34198a22971c6695b575f89449187d2f47 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/groups/GroupMessagesFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/groups/GroupMessagesFragment.kt @@ -9,9 +9,9 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import io.xxlabs.messenger.R import io.xxlabs.messenger.data.room.model.ChatMessage +import io.xxlabs.messenger.data.room.model.GroupData import io.xxlabs.messenger.data.room.model.GroupMessage import io.xxlabs.messenger.support.dialog.MenuChatDialog -import io.xxlabs.messenger.support.dialog.PopupActionBottomDialogFragment import io.xxlabs.messenger.support.extensions.* import io.xxlabs.messenger.support.touch.MessageSwipeController import io.xxlabs.messenger.support.touch.SwipeActions @@ -19,10 +19,24 @@ import io.xxlabs.messenger.ui.main.MainActivity import io.xxlabs.messenger.ui.main.chat.ChatMessagesFragment import io.xxlabs.messenger.ui.main.chat.ChatMessagesUIController import io.xxlabs.messenger.ui.main.chat.adapters.ChatMessagesAdapter +import io.xxlabs.messenger.ui.dialog.warning.showConfirmDialog import javax.inject.Inject class GroupMessagesFragment : ChatMessagesFragment<GroupMessage>() { + private val groupId: ByteArray by lazy { + GroupMessagesFragmentArgs + .fromBundle(requireArguments()) + .groupId + ?.fromBase64toByteArray() + ?: requireArguments().getByteArray("group_id")!! + } + private val group: GroupData? by lazy { + GroupMessagesFragmentArgs + .fromBundle(requireArguments()) + .group + } + /* ViewModels */ @Inject @@ -31,7 +45,8 @@ class GroupMessagesFragment : ChatMessagesFragment<GroupMessage>() { private val chatViewModel: GroupMessagesViewModel by viewModels { GroupMessagesViewModel.provideFactory( chatViewModelFactory, - requireArguments().getByteArray("group_id")!! + groupId, + group ) } override val uiController: ChatMessagesUIController<GroupMessage> diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/groups/GroupMessagesViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/groups/GroupMessagesViewModel.kt index d3abc79bad0e3bfbdf43bb13c2971b5d2de95617..65edbae9cee3dc02ff39b92e40a6b53eac5e3572 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/groups/GroupMessagesViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/groups/GroupMessagesViewModel.kt @@ -41,7 +41,8 @@ class GroupMessagesViewModel @AssistedInject constructor( schedulers: SchedulerProvider, preferences: PreferencesRepository, application: Application, - @Assisted val groupId: ByteArray + @Assisted val groupId: ByteArray, + @Assisted val cachedGroup: GroupData? = null ) : ChatMessagesViewModel<GroupMessage>( repo, daoRepo, schedulers, preferences, application, groupId ) { @@ -61,7 +62,13 @@ class GroupMessagesViewModel @AssistedInject constructor( /* UI */ init { - startSubscription(groupId) + cachedGroup?.let { + groupData.postValue(it) + getGroupMembers(it.groupId) + getMessages(it.groupId) + } ?: run { + startSubscription(groupId) + } } override fun startSubscription(chatId: ByteArray) { @@ -368,10 +375,11 @@ class GroupMessagesViewModel @AssistedInject constructor( companion object { fun provideFactory( assistedFactory: GroupMessagesViewModelFactory, - groupId: ByteArray + groupId: ByteArray, + group: GroupData? = null ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { - return assistedFactory.create(groupId) as T + return assistedFactory.create(groupId, group) as T } } } @@ -561,5 +569,5 @@ value class UserIdToUsernameMap(private val value: MutableMap<UserId, Username>) @AssistedFactory interface GroupMessagesViewModelFactory { - fun create(groupId: ByteArray): GroupMessagesViewModel + fun create(groupId: ByteArray, groupData: GroupData? = null): GroupMessagesViewModel } \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/qrcode/QrCodeViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/qrcode/QrCodeViewModel.kt index ea5f2329d7cca5c7fbefa6663f9022db5e752922..44aeb1649eb87c0381faa2f9593dd5475d9ef394 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/qrcode/QrCodeViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/qrcode/QrCodeViewModel.kt @@ -130,19 +130,19 @@ class QrCodeViewModel @Inject constructor( } fun shouldShareQrCodeEmail(): Boolean { - return preferences.shouldShareEmailQr + return preferences.shareEmailWhenRequesting } fun shouldShareQrCodePhone(): Boolean { - return preferences.shouldSharePhoneQr + return preferences.sharePhoneWhenRequesting } fun setShareQrCodeEmail(checked: Boolean) { - preferences.shouldShareEmailQr = checked + preferences.shareEmailWhenRequesting = checked } fun setShareQrCodePhone(checked: Boolean) { - preferences.shouldSharePhoneQr = checked + preferences.sharePhoneWhenRequesting = checked } fun handleAnalysis( diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsFilter.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsFilter.kt deleted file mode 100644 index 7c6a07a2697fb4e1a2f6efdc2e79dc9802e53720..0000000000000000000000000000000000000000 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/requests/RequestsFilter.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.xxlabs.messenger.ui.main.requests - -enum class RequestsFilter() { - RECEIVED, - SENT, - FAILED; -} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/settings/DeleteAccountFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/settings/DeleteAccountFragment.kt index 293608ee86428d02a132388adff481095eb0d5ea..777891c63fc9aeca732b4fd29d60c8a5edc55056 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/settings/DeleteAccountFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/settings/DeleteAccountFragment.kt @@ -14,9 +14,8 @@ import io.xxlabs.messenger.databinding.FragmentDeleteAccountBinding import io.xxlabs.messenger.support.extensions.setInsets import io.xxlabs.messenger.support.extensions.toast import io.xxlabs.messenger.ui.base.BaseFragment +import io.xxlabs.messenger.ui.dialog.info.showInfoDialog import io.xxlabs.messenger.ui.intro.splash.RegistrationIntroActivity -import io.xxlabs.messenger.ui.intro.splash.SplashScreenLoadingActivity -import io.xxlabs.messenger.ui.intro.splash.SplashScreenPlaceholderActivity import kotlinx.android.synthetic.main.component_toolbar_generic.* import javax.inject.Inject diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/settings/SettingsFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/settings/SettingsFragment.kt index e641cefec238ea570b3b3d226c481446f65ff58c..00368ebd9c4bfb51a0e7c40b23dc622184dcf93a 100755 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/settings/SettingsFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/settings/SettingsFragment.kt @@ -1,19 +1,14 @@ package io.xxlabs.messenger.ui.main.settings import android.content.Intent -import android.content.pm.PackageManager import android.os.Bundle import android.os.Handler import android.os.Looper -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.style.TextAppearanceSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.CompoundButton -import androidx.core.content.ContextCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider @@ -22,14 +17,13 @@ import io.xxlabs.messenger.R import io.xxlabs.messenger.biometrics.BiometricUtils import io.xxlabs.messenger.data.data.DataRequestState import io.xxlabs.messenger.data.data.SimpleRequestState -import io.xxlabs.messenger.support.dialog.BottomSheetPopup -import io.xxlabs.messenger.support.dialog.PopupActionBottomDialogFragment import io.xxlabs.messenger.support.extensions.navigateSafe import io.xxlabs.messenger.support.extensions.setInsets import io.xxlabs.messenger.support.extensions.setOnSingleClickListener import io.xxlabs.messenger.support.util.DialogUtils import io.xxlabs.messenger.support.view.LooperCircularProgressBar import io.xxlabs.messenger.ui.base.BaseFragment +import io.xxlabs.messenger.ui.dialog.info.showInfoDialog import io.xxlabs.messenger.ui.main.MainViewModel import kotlinx.android.synthetic.main.component_toolbar_generic.* import kotlinx.android.synthetic.main.fragment_settings.* diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/ud/profile/UdProfileFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/ud/profile/UdProfileFragment.kt index 569fd97734eda3270904b38a7b819397f5b05716..48e822a32addf718e15e349f379d435e4c83d27e 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/ud/profile/UdProfileFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/ud/profile/UdProfileFragment.kt @@ -18,6 +18,7 @@ import io.xxlabs.messenger.support.extensions.setOnSingleClickListener import io.xxlabs.messenger.support.extensions.toBase64String import io.xxlabs.messenger.support.util.DialogUtils import io.xxlabs.messenger.ui.base.BaseProfileRegistrationFragment +import io.xxlabs.messenger.ui.dialog.warning.showConfirmDialog import kotlinx.android.synthetic.main.fragment_ud_profile.* /** diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchFragment.kt index d6dc9849f039d5a34067a56b49ec661e6282a0a3..c291b8c187a72075563b2c3554e21a90bdf105dd 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchFragment.kt @@ -14,7 +14,11 @@ import androidx.core.os.bundleOf import androidx.core.view.WindowInsetsCompat import androidx.core.view.children import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager @@ -25,15 +29,26 @@ import io.xxlabs.messenger.data.data.Country import io.xxlabs.messenger.data.data.DataRequestState import io.xxlabs.messenger.data.data.SimpleRequestState import io.xxlabs.messenger.data.datatype.NetworkState -import io.xxlabs.messenger.repository.client.ClientRepository +import io.xxlabs.messenger.data.room.model.ContactData +import io.xxlabs.messenger.requests.ui.RequestsViewModel +import io.xxlabs.messenger.requests.ui.nickname.SaveNicknameDialog +import io.xxlabs.messenger.requests.ui.send.OutgoingRequest +import io.xxlabs.messenger.requests.ui.send.SendRequestDialog import io.xxlabs.messenger.support.extensions.* import io.xxlabs.messenger.ui.base.BaseFragment +import io.xxlabs.messenger.ui.dialog.info.showInfoDialog +import io.xxlabs.messenger.ui.dialog.info.showTwoButtonInfoDialog import io.xxlabs.messenger.ui.global.ContactsViewModel import io.xxlabs.messenger.ui.global.NetworkViewModel +import io.xxlabs.messenger.ui.main.MainViewModel import io.xxlabs.messenger.ui.main.countrycode.CountryFullscreenDialog import io.xxlabs.messenger.ui.main.countrycode.CountrySelectionListener import kotlinx.android.synthetic.main.component_toolbar_generic.* import kotlinx.android.synthetic.main.fragment_private_search.* +import io.xxlabs.messenger.ui.dialog.info.showTwoButtonInfoDialog +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import java.util.* import javax.inject.Inject @@ -46,7 +61,12 @@ class UdSearchFragment : BaseFragment() { lateinit var networkViewModel: NetworkViewModel lateinit var contactsViewModel: ContactsViewModel lateinit var udSearchViewModel: UdSearchViewModel + private lateinit var mainViewModel: MainViewModel private lateinit var navController: NavController + private val requestsViewModel: RequestsViewModel by viewModels( + factoryProducer = { viewModelFactory } + ) + private lateinit var resultsAdapter: UdResultAdapter private lateinit var snackBar: Snackbar @@ -59,23 +79,52 @@ class UdSearchFragment : BaseFragment() { private var udSelectionListener = object : UdSelectionListener { override fun onItemSelected(v: View, contactWrapper: ContactWrapperBase) { - v.disableWithAlpha() - currentAddButton = v - val contactString = contactWrapper.marshal().toString(Charsets.ISO_8859_1) - Timber.v("Marshalled String: $contactString") - val bundle = - bundleOf("contact" to contactString) - navController.navigateSafe(R.id.action_ud_search_to_contact_success, bundle) + showSendRequestDialog(contactWrapper) } } + private fun showSendRequestDialog(user: ContactWrapperBase) { + SendRequestDialog + .newInstance(ContactData.from(user)) + .show(childFragmentManager, null) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + observeUI() + } + + } return inflater.inflate(R.layout.fragment_private_search, container, false) } + private fun observeUI() { + requestsViewModel.sendContactRequest.onEach { toUser -> + toUser?.let { + contactsViewModel.updateAndRequestAuthChannel(toUser) + requestsViewModel.onSendRequestHandled() + } + }.launchIn(lifecycleScope) + + requestsViewModel.showCreateNickname.onEach { outgoingRequest -> + outgoingRequest?.let { + showSaveNicknameDialog(it) + requestsViewModel.onShowCreateNicknameHandled() + } + }.launchIn(lifecycleScope) + } + + private fun showSaveNicknameDialog(outgoingRequest: OutgoingRequest) { + SaveNicknameDialog + .newInstance(outgoingRequest) + .show(childFragmentManager, null) + } + override fun onDetach() { snackBar.dismiss() super.onDetach() @@ -85,6 +134,9 @@ class UdSearchFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) navController = findNavController() + mainViewModel = + ViewModelProvider(requireActivity(), viewModelFactory).get(MainViewModel::class.java) + networkViewModel = ViewModelProvider(requireActivity(), viewModelFactory) .get(NetworkViewModel::class.java) @@ -95,6 +147,7 @@ class UdSearchFragment : BaseFragment() { .get(UdSearchViewModel::class.java) initComponents(view) + showNewUserPopups() watchForChanges() } @@ -309,6 +362,62 @@ class UdSearchFragment : BaseFragment() { udSearchResultsRecyclerView.adapter = resultsAdapter } + private fun showNewUserPopups() { + showNotificationDialog() + } + + private fun showNotificationDialog() { + if (preferences.userData.isNotBlank() && preferences.isFirstTimeNotifications) { + showTwoButtonInfoDialog( + title = R.string.settings_push_notifications_dialog_title, + body = R.string.settings_push_notifications_dialog_body, + linkTextToUrlMap = null, + positiveClick = ::enablePushNotifications, + negativeClick = null, + onDismiss = ::showCoverMessageDialog + ) + preferences.isFirstTimeNotifications = false + } + } + + private fun enablePushNotifications() { + mainViewModel.enableNotifications { error -> + error?.let { showError(error) } + } + } + + private fun showCoverMessageDialog() { + if (preferences.userData.isNotBlank() && preferences.isFirstTimeCoverMessages) { + showTwoButtonInfoDialog( + R.string.settings_cover_traffic_title, + R.string.settings_cover_traffic_dialog_body, + mapOf( + getString(R.string.settings_cover_traffic_link_text) + to getString(R.string.settings_cover_traffic_link_url) + ), + ::enableCoverMessages, + ::declineCoverMessages, + ) + preferences.isFirstTimeCoverMessages = false + } + } + + private fun enableCoverMessages() { + enableDummyTraffic(true) + } + + private fun declineCoverMessages() { + enableDummyTraffic(false) + } + + private fun enableDummyTraffic(enabled: Boolean) { + try { + mainViewModel.enableDummyTraffic(enabled) + } catch (e: Exception) { + showError(e, true) + } + } + private fun watchForChanges() { networkViewModel.networkState.observe(viewLifecycleOwner, { networkState -> if (networkState != NetworkState.HAS_CONNECTION) { diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchViewModel.kt index 31d8c8c4f27afd87cccbca47b70a3c165e51eb82..fdb8d61da8bc21060c67369ed356a9fc3077887d 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchViewModel.kt @@ -8,6 +8,7 @@ import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase import io.xxlabs.messenger.data.data.ContactSearchResult import io.xxlabs.messenger.data.data.DataRequestState import io.xxlabs.messenger.data.datatype.FactType +import io.xxlabs.messenger.data.room.model.ContactData import io.xxlabs.messenger.repository.DaoRepository import io.xxlabs.messenger.repository.base.BaseRepository import io.xxlabs.messenger.support.extensions.fromBase64toByteArray @@ -22,7 +23,7 @@ class UdSearchViewModel @Inject constructor( private val schedulers: SchedulerProvider ) : ViewModel() { var subscriptions = CompositeDisposable() - val searchState = MutableLiveData<DataRequestState<ByteArray>>() + val searchState = MutableLiveData<DataRequestState<ContactData>>() val contactResult = MutableLiveData<ContactSearchResult>() //Registration Step State diff --git a/app/src/main/res/color/selector_qr_code_tab_secondary.xml b/app/src/main/res/color/selector_qr_code_tab_secondary.xml index 8d469566c88b43be95da541a2f65536cd37c2a0e..b37f70ad96830108ec673a1efa4c4c227f9f1727 100644 --- a/app/src/main/res/color/selector_qr_code_tab_secondary.xml +++ b/app/src/main/res/color/selector_qr_code_tab_secondary.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@color/brand_dark" android:state_selected="true" /> + <item android:color="@color/brand_default" android:state_selected="true" /> <item android:color="@color/neutral_weak" /> </selector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml index af679186bc74cf83ba5cff0c6fbf88b701e160ec..09752b3311f3da7247dd229700f75d053cfb2e72 100644 --- a/app/src/main/res/drawable/ic_check.xml +++ b/app/src/main/res/drawable/ic_check.xml @@ -5,5 +5,5 @@ android:viewportHeight="14"> <path android:pathData="M17.8388,1.6947L5.818,13.7155L0.1611,8.0586L1.5711,6.6486L5.818,10.8855L16.4288,0.2847L17.8388,1.6947Z" - android:fillColor="#037281"/> + android:fillColor="@color/accent_success"/> </vector> diff --git a/app/src/main/res/drawable/ic_danger.xml b/app/src/main/res/drawable/ic_danger.xml index f3efb20986d6019fb95588a12b63c1c6b21d8c37..6a7453f775b1ee58efec9b4b267ca98b37fd1552 100644 --- a/app/src/main/res/drawable/ic_danger.xml +++ b/app/src/main/res/drawable/ic_danger.xml @@ -7,13 +7,13 @@ android:pathData="M11.1429,3L20.2857,19H2L11.1429,3Z" android:strokeWidth="2" android:fillColor="#00000000" - android:strokeColor="#D12D4B"/> + android:strokeColor="#242424"/> <path - android:pathData="M11.1428,8.7144V14.0477" + android:pathData="M11.1428,8.7139V14.0472" android:strokeWidth="2" android:fillColor="#00000000" - android:strokeColor="#D12D4B"/> + android:strokeColor="#242424"/> <path android:pathData="M11.1428,16.7142C11.5636,16.7142 11.9047,16.3731 11.9047,15.9523C11.9047,15.5315 11.5636,15.1904 11.1428,15.1904C10.722,15.1904 10.3809,15.5315 10.3809,15.9523C10.3809,16.3731 10.722,16.7142 11.1428,16.7142Z" - android:fillColor="#D12D4B"/> + android:fillColor="#242424"/> </vector> diff --git a/app/src/main/res/drawable/ic_group_chat.xml b/app/src/main/res/drawable/ic_group_chat.xml new file mode 100644 index 0000000000000000000000000000000000000000..2f9f20c0e90dde5b4155fdc45cf75f998d271602 --- /dev/null +++ b/app/src/main/res/drawable/ic_group_chat.xml @@ -0,0 +1,14 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="12dp" + android:height="12dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group> + <clip-path + android:pathData="M0,0h24v24h-24z"/> + <path + android:pathData="M7.3848,8.6154C7.3848,6.0664 9.4512,4 12.0002,4C14.5492,4 16.6155,6.0664 16.6155,8.6154C16.6155,11.1644 14.5492,13.2307 12.0002,13.2307C9.4512,13.2307 7.3848,11.1644 7.3848,8.6154ZM12.0002,5.8461C10.4708,5.8461 9.231,7.086 9.231,8.6154C9.231,10.1448 10.4708,11.3846 12.0002,11.3846C13.5296,11.3846 14.7694,10.1448 14.7694,8.6154C14.7694,7.086 13.5296,5.8461 12.0002,5.8461ZM18.4617,8.6154C18.7532,8.6154 19.0405,8.6844 19.3002,8.8168C19.5599,8.9493 19.7846,9.1413 19.9558,9.3772C20.127,9.6131 20.2399,9.8862 20.2853,10.1741C20.3306,10.4621 20.3072,10.7567 20.2168,11.0338C20.1265,11.311 19.9717,11.5628 19.7654,11.7686C19.559,11.9744 19.3068,12.1285 19.0294,12.2181C18.752,12.3078 18.4574,12.3305 18.1695,12.2843C17.8817,12.2382 17.6089,12.1246 17.3734,11.9528L16.2852,13.4441L16.2861,13.4448C16.7568,13.7881 17.3021,14.0151 17.8774,14.1073C17.9408,14.1174 18.0044,14.1259 18.068,14.1328C18.5825,14.1879 19.1035,14.1344 19.5971,13.9749C20.1519,13.7956 20.6563,13.4875 21.0691,13.0758C21.4819,12.6641 21.7913,12.1605 21.9721,11.6062C22.1528,11.0519 22.1997,10.4627 22.109,9.8868C22.0182,9.3109 21.7924,8.7646 21.4499,8.2928C21.1452,7.8729 20.7559,7.5226 20.3078,7.2639C20.2524,7.2319 20.196,7.2013 20.1388,7.1721C19.6194,6.9073 19.0447,6.7693 18.4617,6.7693V8.6154ZM22.1522,20.6153C22.1522,20.1307 22.0568,19.6508 21.8713,19.203C21.6858,18.7552 21.414,18.3484 21.0713,18.0057C20.7286,17.663 20.3217,17.3912 19.874,17.2057C19.4262,17.0202 18.9463,16.9248 18.4617,16.9248V15.0769C19.0913,15.0769 19.7156,15.1842 20.3078,15.3936C20.3997,15.4261 20.4909,15.4611 20.5812,15.4985C21.2531,15.7768 21.8637,16.1847 22.3779,16.699C22.8922,17.2133 23.3002,17.8239 23.5785,18.4958C23.6159,18.5861 23.6509,18.6772 23.6834,18.7692C23.8928,19.3614 24.0001,19.9857 24.0001,20.6153H22.1522ZM16.6155,20.6153H18.4617C18.4617,17.0467 15.5688,14.1538 12.0002,14.1538C8.4316,14.1538 5.5387,17.0467 5.5387,20.6153H7.3848C7.3848,18.0663 9.4512,15.9999 12.0002,15.9999C14.5492,15.9999 16.6155,18.0663 16.6155,20.6153ZM5.5384,8.6147C5.2469,8.6147 4.9596,8.6837 4.6999,8.8161C4.4402,8.9485 4.2156,9.1405 4.0444,9.3764C3.8731,9.6124 3.7602,9.8855 3.7148,10.1734C3.6695,10.4614 3.6929,10.756 3.7833,11.0331C3.8737,11.3102 4.0284,11.562 4.2348,11.7679C4.4412,11.9737 4.6933,12.1278 4.9707,12.2174C5.2481,12.307 5.5427,12.3297 5.8306,12.2836C6.1184,12.2375 6.3912,12.1239 6.6267,11.9521L7.715,13.4433L7.714,13.444C7.2433,13.7873 6.698,14.0143 6.1227,14.1065C6.0593,14.1167 5.9957,14.1252 5.9321,14.132C5.4177,14.1872 4.8966,14.1337 4.403,13.9741C3.8482,13.7949 3.3438,13.4867 2.931,13.075C2.5182,12.6633 2.2088,12.1597 2.0281,11.6054C1.8473,11.0512 1.8004,10.4619 1.8911,9.886C1.9819,9.3101 2.2077,8.7639 2.5502,8.2921C2.8549,7.8722 3.2442,7.5218 3.6923,7.2632C3.7478,7.2311 3.8041,7.2005 3.8613,7.1714C4.3807,6.9065 4.9554,6.7685 5.5384,6.7685V8.6147ZM2.1288,19.2023C1.9434,19.65 1.8479,20.1299 1.8479,20.6146H0C0,19.985 0.1074,19.3607 0.3167,18.7684C0.3492,18.6765 0.3842,18.5854 0.4216,18.4951C0.6999,17.8232 1.1079,17.2126 1.6222,16.6983C2.1365,16.184 2.747,15.7761 3.419,15.4977C3.5092,15.4603 3.6004,15.4254 3.6923,15.3929C4.2846,15.1835 4.9088,15.0761 5.5384,15.0761V16.924C5.0538,16.924 4.5739,17.0195 4.1261,17.205C3.6784,17.3904 3.2715,17.6623 2.9288,18.005C2.5861,18.3477 2.3143,18.7545 2.1288,19.2023Z" + android:fillColor="@color/neutral_white" + android:fillType="evenOdd"/> + </group> +</vector> diff --git a/app/src/main/res/drawable/ic_mail_received.xml b/app/src/main/res/drawable/ic_mail_received.xml new file mode 100644 index 0000000000000000000000000000000000000000..32b19003433cbcc393fde2b936ee300c0b4d4883 --- /dev/null +++ b/app/src/main/res/drawable/ic_mail_received.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group> + <clip-path + android:pathData="M0,0h24v24h-24z"/> + <path + android:pathData="M21,9.9V17.6C21,18.5941 20.1941,19.4 19.2,19.4H4.8C3.8059,19.4 3,18.5941 3,17.6V6.7217C3.0419,5.7583 3.8357,4.9991 4.8,5H15C15,5.6346 15.1182,6.2415 15.3338,6.8H5.52L12,11.12L16.2386,8.2943C16.6565,8.7711 17.1637,9.1678 17.734,9.4583L12,13.28L4.8,8.4812V17.6H19.2V9.9363C19.4605,9.9782 19.7277,10 20,10C20.3425,10 20.6769,9.9656 21,9.9Z" + android:fillColor="#242424" + android:fillType="evenOdd"/> + <path + android:pathData="M24,4L21.45,4L21.45,1L18.55,1L18.55,4L16,4L20,8L24,4Z" + android:fillColor="#242424" + android:fillType="evenOdd"/> + </group> +</vector> diff --git a/app/src/main/res/drawable/ic_mail_sent.xml b/app/src/main/res/drawable/ic_mail_sent.xml new file mode 100644 index 0000000000000000000000000000000000000000..f0020f52a65c60e82b46b9e0086e4067e26d1e5e --- /dev/null +++ b/app/src/main/res/drawable/ic_mail_sent.xml @@ -0,0 +1,14 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M21,9.9V17.6C21,18.5941 20.1941,19.4 19.2,19.4H4.8C3.8059,19.4 3,18.5941 3,17.6V6.7217C3.0419,5.7583 3.8357,4.9991 4.8,5H15C15,5.6346 15.1182,6.2415 15.3338,6.8H5.52L12,11.12L16.2386,8.2943C16.6565,8.7711 17.1637,9.1678 17.734,9.4583L12,13.28L4.8,8.4812V17.6H19.2V9.9363C19.4605,9.9782 19.7277,10 20,10C20.3425,10 20.6769,9.9656 21,9.9Z" + android:fillColor="#242424" + android:fillType="evenOdd"/> + <path + android:pathData="M16,5H18.55V8H21.45V5H24L20,1L16,5Z" + android:fillColor="#242424" + android:fillType="evenOdd"/> +</vector> diff --git a/app/src/main/res/drawable/ic_retry.xml b/app/src/main/res/drawable/ic_retry.xml new file mode 100644 index 0000000000000000000000000000000000000000..68e186bdfd0533933aaf518117eb0a53413fc90f --- /dev/null +++ b/app/src/main/res/drawable/ic_retry.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="15dp" + android:height="21dp" + android:viewportWidth="15" + android:viewportHeight="21"> + <path + android:pathData="M15,10.4732C15,8.5253 14.382,7.0763 12.934,5.6293C12.543,5.2393 11.911,5.2393 11.52,5.6293C11.129,6.0202 11.129,6.6533 11.52,7.0443C12.599,8.1222 13,9.0513 13,10.4732C13,11.9422 12.428,13.3232 11.389,14.3612C10.385,15.3643 9.311,15.8633 7.961,15.9543L9.207,14.7073C9.598,14.3163 9.598,13.6842 9.207,13.2933C8.816,12.9023 8.184,12.9023 7.793,13.2933L4.086,17.0002L7.793,20.7073C7.988,20.9023 8.244,21.0002 8.5,21.0002C8.756,21.0002 9.012,20.9023 9.207,20.7073C9.598,20.3162 9.598,19.6842 9.207,19.2932L7.87,17.9573C9.793,17.8752 11.412,17.1652 12.803,15.7762C14.22,14.3602 15,12.4772 15,10.4732ZM2,10.5002C2,9.0313 2.572,7.6502 3.611,6.6112C4.62,5.6023 5.703,5.1033 7.068,5.0173L5.793,6.2923C5.402,6.6832 5.402,7.3152 5.793,7.7063C5.988,7.9022 6.244,8.0002 6.5,8.0002C6.756,8.0002 7.012,7.9022 7.207,7.7073L10.914,4.0002L7.207,0.2932C6.816,-0.0978 6.184,-0.0978 5.793,0.2932C5.402,0.6842 5.402,1.3162 5.793,1.7072L7.104,3.0182C5.19,3.1042 3.579,3.8142 2.197,5.1972C0.78,6.6132 0,8.4963 0,10.5002C0,12.4482 0.618,13.8972 2.066,15.3442C2.261,15.5392 2.517,15.6362 2.773,15.6362C3.029,15.6362 3.285,15.5382 3.48,15.3433C3.871,14.9523 3.871,14.3193 3.48,13.9283C2.401,12.8513 2,11.9222 2,10.5002Z" + android:fillColor="#242424"/> +</vector> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 071575105da735a6437e9c2a8f29aff71f79f172..07c988ad34041be051eca8df632d56f40cfc0728 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -27,6 +27,11 @@ app:cardPreventCornerOverlap="false" app:cardUseCompatPadding="false"> + <FrameLayout + android:id="@+id/customToastView" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + <fragment android:id="@+id/mainNavHost" android:name="androidx.navigation.fragment.NavHostFragment" diff --git a/app/src/main/res/layout/component_close_button.xml b/app/src/main/res/layout/component_close_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..7d0814e856cb26608e53dd070552f0bd2e4488b3 --- /dev/null +++ b/app/src/main/res/layout/component_close_button.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.ui.dialog.components.CloseButtonUI" /> + </data> + + <ImageButton + android:id="@+id/close_button" + android:layout_width="65dp" + android:layout_height="65dp" + android:adjustViewBounds="true" + android:scaleType="centerCrop" + app:srcCompat="@drawable/ic_close_dark" + style="@style/Widget.AppCompat.Button.Borderless" /> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_custom_toast.xml b/app/src/main/res/layout/component_custom_toast.xml new file mode 100644 index 0000000000000000000000000000000000000000..fb45981e4e395eab54ab3a9adb338afa737b497e --- /dev/null +++ b/app/src/main/res/layout/component_custom_toast.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.support.toast.ToastUI" /> + </data> + + <androidx.cardview.widget.CardView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:backgroundTint="@{ui.backgroundColor}" + android:layout_margin="16dp" + app:cardCornerRadius="16dp" + app:cardElevation="5dp" + tools:backgroundTint="@color/modal_overlay"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="24dp"> + + <ImageView + android:id="@+id/toast_left_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:visibility="@{ui.leftIcon != null}" + app:actionIcon="@{ui.leftIcon}" + app:actionIconTint="@{ui.iconTint}" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:src="@drawable/ic_check" /> + + <TextView + android:id="@+id/toast_header_text" + style="@style/custom_toast_header" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:maxLines="1" + android:visibility="@{ui.header}" + android:text="@{ui.header}" + android:layout_marginHorizontal="8dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/toast_left_icon" + app:layout_constraintEnd_toStartOf="@id/toast_action_text" + app:layout_constraintBottom_toTopOf="@id/toast_body_text" + tools:text="Percyking" /> + + <TextView + android:id="@+id/toast_body_text" + style="@style/custom_toast_body" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:maxLines="2" + android:visibility="@{ui.body}" + android:text="@{ui.body}" + android:layout_marginHorizontal="8dp" + app:layout_constraintTop_toBottomOf="@id/toast_header_text" + app:layout_constraintStart_toEndOf="@id/toast_left_icon" + app:layout_constraintEnd_toStartOf="@id/toast_action_text" + app:layout_constraintBottom_toBottomOf="parent" + tools:text="Accepted your request" /> + + <TextView + android:id="@+id/toast_action_text" + style="@style/toast_action_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:maxLines="2" + android:visibility="@{ui.actionText}" + android:text="@{ui.actionText}" + android:onClick="@{() -> ui.onActionClick()}" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + tools:text="SEND\nMESSAGE" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.cardview.widget.CardView> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_info_dialog.xml b/app/src/main/res/layout/component_info_dialog.xml index 89b04808f131940797353b59860e59cc567843bc..2fc676706cd58b8ad60b4a96b532b55c3c5db5d9 100644 --- a/app/src/main/res/layout/component_info_dialog.xml +++ b/app/src/main/res/layout/component_info_dialog.xml @@ -7,7 +7,7 @@ <data> <variable name="ui" - type="io.xxlabs.messenger.support.dialog.info.InfoDialogUI" /> + type="io.xxlabs.messenger.ui.dialog.info.InfoDialogUI" /> </data> <androidx.cardview.widget.CardView diff --git a/app/src/main/res/layout/component_invitation_details_dialog.xml b/app/src/main/res/layout/component_invitation_details_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..aada5a2cdc437e1256ad985c916a3d3d97d3ee08 --- /dev/null +++ b/app/src/main/res/layout/component_invitation_details_dialog.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.details.group.InvitationDetailsUI" /> + </data> + + <androidx.cardview.widget.CardView + android:id="@+id/invitation_details_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:cardCornerRadius="@dimen/bottom_sheet_dialog_corner_radius"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <include + layout="@layout/component_close_button" + android:id="@+id/close_button_layout" + android:layout_width="65dp" + android:layout_height="65dp" + app:ui="@{ui}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/invitation_details_dialog_title" + style="@style/request_details_dialog_title" + android:text="@string/group_invitation_title" + app:layout_constraintBottom_toTopOf="@id/invitation_details_group_name" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/close_button_layout" /> + + <TextView + android:id="@+id/invitation_details_group_name" + style="@style/request_details_dialog_subtitle" + android:layout_marginVertical="24dp" + android:layout_marginTop="16dp" + android:text="@{ui.groupName}" + app:layout_constraintBottom_toTopOf="@id/invitation_details_members_list" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/invitation_details_dialog_title" + tools:text="Family Group" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/invitation_details_members_list" + style="@style/invitation_details_members_list" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="@dimen/spacing_32" + app:layout_constrainedHeight="true" + app:layout_constraintBottom_toTopOf="@id/dialog_button_layout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/invitation_details_group_name" /> + + <ProgressBar + android:id="@+id/invitiation_details_progressbar" + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:progressTint="@color/brand_default" + android:visibility="@{ui.isLoading()}" + app:layout_constraintBottom_toTopOf="@id/dialog_button_layout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/invitation_details_group_name" /> + + <include + layout="@layout/component_vertical_positive_negative_button" + android:id="@+id/dialog_button_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:ui="@{ui}" + app:layout_constraintTop_toBottomOf="@id/invitation_details_members_list" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.cardview.widget.CardView> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_item_thumbnail.xml b/app/src/main/res/layout/component_item_thumbnail.xml new file mode 100644 index 0000000000000000000000000000000000000000..6fc1c53fcc52167dbe2d135f03151a6ef9babd95 --- /dev/null +++ b/app/src/main/res/layout/component_item_thumbnail.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.list.adapter.ItemThumbnail" /> + </data> + + <androidx.cardview.widget.CardView + android:id="@+id/thumbnailLayout" + android:layout_width="@dimen/spacing_42" + android:layout_height="@dimen/spacing_42" + android:backgroundTint="@color/brand_default" + app:cardPreventCornerOverlap="false" + app:cardCornerRadius="16dp" + app:cardElevation="0dp"> + + <TextView + android:id="@+id/thumbnailText" + style="@style/XxTextStyle.Bold" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_centerInParent="true" + android:layout_gravity="center" + android:gravity="center" + android:textColor="@color/neutral_white" + android:visibility="@{ui.itemInitials}" + android:text="@{ui.itemInitials}" + tools:text="BP" + tools:visibility="gone"/> + + <ImageView + android:id="@+id/thumbnailImage" + android:layout_width="@dimen/spacing_24" + android:layout_height="@dimen/spacing_24" + android:layout_gravity="center" + android:adjustViewBounds="true" + android:visibility="@{ui.itemPhoto != null || ui.itemIconRes != null}" + app:thumbnailBitmap="@{ui.itemPhoto}" + app:thumbnailIcon="@{ui.itemIconRes}" + tools:srcCompat="@drawable/ic_group_chat"/> + </androidx.cardview.widget.CardView> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_new_action_dialog.xml b/app/src/main/res/layout/component_new_action_dialog.xml index 94ac0231e20f13a843a8e7fe3c6e3621d91fc889..df4e4494b9b96ddee24031ed2efc5b64f95ee5f1 100644 --- a/app/src/main/res/layout/component_new_action_dialog.xml +++ b/app/src/main/res/layout/component_new_action_dialog.xml @@ -7,7 +7,7 @@ <data> <variable name="ui" - type="io.xxlabs.messenger.support.dialog.action.ActionDialogUI" /> + type="io.xxlabs.messenger.ui.dialog.action.ActionDialogUI" /> </data> <androidx.cardview.widget.CardView diff --git a/app/src/main/res/layout/component_radiobutton_dialog.xml b/app/src/main/res/layout/component_radiobutton_dialog.xml index a4f6fa0bb430d82ef411957ed5320160d598c057..62f7f43610d856e05e8e77b04266917f40bbfc39 100644 --- a/app/src/main/res/layout/component_radiobutton_dialog.xml +++ b/app/src/main/res/layout/component_radiobutton_dialog.xml @@ -7,7 +7,7 @@ <data> <variable name="ui" - type="io.xxlabs.messenger.backup.ui.save.RadioButtonDialogUI" /> + type="io.xxlabs.messenger.ui.dialog.radiobutton.RadioButtonDialogUI" /> </data> <androidx.cardview.widget.CardView diff --git a/app/src/main/res/layout/component_request_accepted_dialog.xml b/app/src/main/res/layout/component_request_accepted_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..69de1bdd55b19d7b6b6e3f874c8b5a58565aa42f --- /dev/null +++ b/app/src/main/res/layout/component_request_accepted_dialog.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.accepted.RequestAcceptedUI" /> + </data> + + <androidx.cardview.widget.CardView + android:id="@+id/requestAcceptedLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:cardCornerRadius="@dimen/bottom_sheet_dialog_corner_radius"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <include + layout="@layout/component_close_button" + android:id="@+id/close_button_layout" + android:layout_width="65dp" + android:layout_height="65dp" + app:ui="@{ui}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/request_accepted_title" + android:text="@{ui.title}" + android:textColor="@color/accent_success" + app:drawableLeftCompat="@drawable/ic_check" + app:drawableTint="@color/accent_success" + android:drawablePadding="12dp" + app:layout_constraintTop_toBottomOf="@id/close_button_layout" + app:layout_constraintBottom_toTopOf="@id/request_accepted_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + style="@style/request_details_dialog_title" + tools:text="new connection" /> + + <TextView + android:id="@+id/request_accepted_subtitle" + style="@style/request_details_dialog_subtitle" + android:layout_marginVertical="16dp" + android:layout_marginTop="16dp" + android:text="@{ui.subtitle}" + app:layout_constraintTop_toBottomOf="@id/request_accepted_title" + app:layout_constraintBottom_toTopOf="@id/request_accepted_body" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:text="Jane Huntington" /> + + <TextView + android:id="@+id/request_accepted_body" + style="@style/dialog_body" + android:layout_marginVertical="16dp" + android:layout_marginTop="16dp" + android:text="@{ui.body}" + app:layout_constraintTop_toBottomOf="@id/request_accepted_subtitle" + app:layout_constraintBottom_toTopOf="@id/dialog_button_layout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:text="Is now a connection, would you like to send a message?" /> + + <include + layout="@layout/component_vertical_positive_negative_button" + android:id="@+id/dialog_button_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:ui="@{ui}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.cardview.widget.CardView> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_request_details_dialog.xml b/app/src/main/res/layout/component_request_details_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..8e715b487a7ac6b798dec06e1dd6ed6c5c4e0ce8 --- /dev/null +++ b/app/src/main/res/layout/component_request_details_dialog.xml @@ -0,0 +1,141 @@ +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.details.contact.RequestDetailsUI" /> + </data> + + <androidx.cardview.widget.CardView + android:id="@+id/requestDetailsDialogLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:cardCornerRadius="@dimen/bottom_sheet_dialog_corner_radius"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <include + android:id="@+id/close_button_layout" + layout="@layout/component_close_button" + android:layout_width="65dp" + android:layout_height="65dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:ui="@{ui}" /> + + <TextView + android:id="@+id/request_details_dialog_title" + style="@style/request_details_dialog_title" + android:text="@string/request_details_title" + app:layout_constraintBottom_toTopOf="@id/request_details_username" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/close_button_layout" /> + + <TextView + android:id="@+id/request_details_username" + style="@style/request_details_dialog_subtitle" + android:layout_marginVertical="24dp" + android:layout_marginTop="16dp" + android:text="@{ui.username}" + app:layout_constraintBottom_toTopOf="@id/request_details_email_header" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/request_details_dialog_title" + tools:text="janehunt" /> + + <TextView + android:id="@+id/request_details_email_header" + style="@style/request_details_section_header" + android:layout_marginBottom="12dp" + android:visibility="@{ui.email}" + android:text="@string/request_details_email_header" + app:layout_constraintBottom_toTopOf="@id/request_details_email" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/request_details_email" + style="@style/request_details_section_content" + android:layout_marginBottom="32dp" + android:visibility="@{ui.email}" + android:text="@{ui.email}" + app:layout_constraintBottom_toTopOf="@id/request_details_phone_header" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:text="jane.huntington@gmail.com" /> + + <TextView + android:id="@+id/request_details_phone_header" + style="@style/request_details_section_header" + android:layout_marginBottom="12dp" + android:visibility="@{ui.phone}" + android:text="@string/request_details_phone_header" + app:layout_constraintBottom_toTopOf="@id/request_details_phone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/request_details_phone" + style="@style/request_details_section_content" + android:layout_marginBottom="32dp" + android:visibility="@{ui.phone}" + android:text="@{ui.phone}" + app:layout_constraintBottom_toTopOf="@id/request_details_nickname_section" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:text="+1 310-123-4567" /> + + <TextView + android:id="@+id/request_details_nickname_section" + style="@style/request_details_textinput_label" + android:layout_marginVertical="@dimen/registration_body_vertical_margin" + android:text="@string/request_details_nickname_section" + app:layout_constraintBottom_toTopOf="@id/request_details_nickname_input" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:text="Edit your new contact’s nickname." /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/request_details_nickname_input" + style="@style/registration_text_input" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginVertical="@dimen/registration_body_vertical_margin" + android:error="@{ui.nicknameError}" + app:endIconMode="custom" + app:endIconDrawable="@drawable/ic_check" + app:endIconTint="@color/accent_success" + app:layout_constraintBottom_toTopOf="@id/dialog_button_layout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:hint="janehunt"> + + <com.google.android.material.textfield.TextInputEditText + style="@style/registration_text_input_edittext" + android:hint="@{ui.nicknameHint}" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:afterTextChanged="@{ui::onNicknameInput}" + android:imeOptions="actionDone" + android:maxLength="@{ui.maxNicknameLength}" /> + </com.google.android.material.textfield.TextInputLayout> + + <include + android:id="@+id/dialog_button_layout" + layout="@layout/component_vertical_positive_negative_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:ui="@{ui}" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.cardview.widget.CardView> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_send_request_dialog.xml b/app/src/main/res/layout/component_send_request_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..6c6103d159d1131591185308968f205901045af8 --- /dev/null +++ b/app/src/main/res/layout/component_send_request_dialog.xml @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.send.SendRequestUI" /> + </data> + + <androidx.cardview.widget.CardView + android:id="@+id/send_request_dialog_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:cardCornerRadius="@dimen/bottom_sheet_dialog_corner_radius"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include + android:id="@+id/close_button_layout" + layout="@layout/component_close_button" + android:layout_width="65dp" + android:layout_height="65dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:ui="@{ui}" /> + + <TextView + android:id="@+id/send_request_dialog_title" + style="@style/bold_title" + android:text="@string/send_request_dialog_title" + android:textSize="26sp" + android:layout_marginTop="0dp" + android:textColor="@color/neutral_dark" + android:fontWeight="800" + app:layout_constraintBottom_toTopOf="@id/send_request_dialog_body" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/close_button_layout" + tools:text="Request Contact" /> + + <TextView + android:id="@+id/send_request_dialog_body" + style="@style/dialog_body" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@{ui.body}" + android:textSize="16sp" + app:layout_constraintTop_toBottomOf="@id/send_request_dialog_title" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@id/send_request_email_header" + tools:text="Share your information with Percyking (percy.king@gmail.com) so they know it’s you." /> + + <TextView + android:id="@+id/send_request_email_header" + style="@style/request_details_section_header" + android:layout_marginBottom="12dp" + android:text="@string/request_details_email_header" + android:layout_marginTop="24dp" + android:layout_marginEnd="16dp" + app:layout_constraintTop_toBottomOf="@id/send_request_dialog_body" + app:layout_constraintBottom_toTopOf="@id/send_request_sender_email" + app:layout_constraintEnd_toStartOf="@id/send_request_email_toggle" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/send_request_sender_email" + style="@style/request_details_section_content" + android:layout_marginBottom="32dp" + android:text="@{ui.senderEmail}" + android:layout_marginEnd="16dp" + app:layout_constraintTop_toBottomOf="@id/send_request_email_header" + app:layout_constraintBottom_toTopOf="@id/send_request_phone_header" + app:layout_constraintEnd_toStartOf="@id/send_request_email_toggle" + app:layout_constraintStart_toStartOf="parent" + tools:text="jane.huntington@gmail.com" /> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/send_request_email_toggle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_marginEnd="@dimen/registration_horizontal_margin" + android:enabled="@{ui.emailToggleEnabled}" + android:onCheckedChanged="@{(_, bool)-> ui.onEmailToggled(bool)}" + app:layout_constraintTop_toTopOf="@id/send_request_email_header" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/send_request_sender_email"/> + + <TextView + android:id="@+id/send_request_phone_header" + style="@style/request_details_section_header" + android:layout_marginBottom="12dp" + android:text="@string/request_details_phone_header" + android:layout_marginEnd="16dp" + app:layout_constraintTop_toBottomOf="@id/send_request_sender_email" + app:layout_constraintBottom_toTopOf="@id/send_request_sender_phone" + app:layout_constraintEnd_toStartOf="@id/send_request_phone_toggle" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/send_request_sender_phone" + style="@style/request_details_section_content" + android:layout_marginBottom="32dp" + android:text="@{ui.senderPhone}" + android:layout_marginEnd="16dp" + app:layout_constraintTop_toBottomOf="@id/send_request_phone_header" + app:layout_constraintBottom_toTopOf="@id/dialog_button_layout" + app:layout_constraintEnd_toStartOf="@id/send_request_phone_toggle" + app:layout_constraintStart_toStartOf="parent" + tools:text="+1 310-123-4567" /> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/send_request_phone_toggle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_marginEnd="@dimen/registration_horizontal_margin" + android:enabled="@{ui.phoneToggleEnabled}" + android:onCheckedChanged="@{(_, bool)-> ui.onPhoneToggled(bool)}" + app:layout_constraintTop_toTopOf="@id/send_request_phone_header" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/send_request_sender_phone"/> + + <include + android:id="@+id/dialog_button_layout" + layout="@layout/component_vertical_positive_negative_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:ui="@{ui}" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.cardview.widget.CardView> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_send_request_nickname.xml b/app/src/main/res/layout/component_send_request_nickname.xml new file mode 100644 index 0000000000000000000000000000000000000000..d4e5c7ac21e192f56e4afcf7349cf7f0ad40aadf --- /dev/null +++ b/app/src/main/res/layout/component_send_request_nickname.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.nickname.SaveNicknameUI" /> + </data> + + <androidx.cardview.widget.CardView + android:id="@+id/requestDetailsDialogLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:cardCornerRadius="@dimen/bottom_sheet_dialog_corner_radius"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <include + android:id="@+id/close_button_layout" + layout="@layout/component_close_button" + android:layout_width="65dp" + android:layout_height="65dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:ui="@{ui}" /> + + <TextView + android:id="@+id/save_nickname_dialog_title" + style="@style/request_details_dialog_subtitle" + android:text="@string/save_nickname_title" + app:layout_constraintBottom_toTopOf="@id/save_nickname_dialog_body" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/close_button_layout" /> + + <TextView + android:id="@+id/save_nickname_dialog_body" + style="@style/dialog_body" + android:layout_marginVertical="@dimen/registration_body_vertical_margin" + android:text="@string/save_nickname_dialog_body" + app:layout_constraintBottom_toTopOf="@id/save_nickname_input" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/save_nickname_dialog_title" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/save_nickname_input" + style="@style/registration_text_input" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginVertical="@dimen/registration_body_vertical_margin" + android:error="@{ui.nicknameError}" + app:endIconMode="custom" + app:endIconDrawable="@drawable/ic_check" + app:endIconTint="@color/accent_success" + app:layout_constraintTop_toBottomOf="@id/save_nickname_dialog_body" + app:layout_constraintBottom_toTopOf="@id/save_nickname_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:hint="janehunt"> + + <com.google.android.material.textfield.TextInputEditText + style="@style/registration_text_input_edittext" + android:hint="@{ui.nicknameHint}" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:afterTextChanged="@{ui::onNicknameInput}" + android:imeOptions="actionDone" + android:maxLength="@{ui.maxNicknameLength}" /> + </com.google.android.material.textfield.TextInputLayout> + + <Button + android:id="@+id/save_nickname_button" + style="@style/request_details_dialog_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginBottom="50dp" + android:layout_marginHorizontal="@dimen/registration_horizontal_margin" + android:text="@string/save_nickname_positive_button" + android:textColor="@color/neutral_white" + android:background="@drawable/bg_btn_white" + android:backgroundTint="@color/brand_default" + android:enabled="@{ui.positiveButtonEnabled}" + app:layout_constraintTop_toBottomOf="@id/save_nickname_input" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.cardview.widget.CardView> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_edittext_two_button_dialog.xml b/app/src/main/res/layout/component_textinput_dialog.xml similarity index 98% rename from app/src/main/res/layout/component_edittext_two_button_dialog.xml rename to app/src/main/res/layout/component_textinput_dialog.xml index b476a22a56b899f4bbb5b9a3583afdbdb8e52b25..36213eee4f89976af991c91b1d7eb7996e85fe90 100644 --- a/app/src/main/res/layout/component_edittext_two_button_dialog.xml +++ b/app/src/main/res/layout/component_textinput_dialog.xml @@ -6,7 +6,7 @@ <data> <variable name="ui" - type="io.xxlabs.messenger.backup.ui.save.EditTextTwoButtonDialogUI" /> + type="io.xxlabs.messenger.ui.dialog.textinput.TextInputDialogUI" /> </data> <androidx.cardview.widget.CardView diff --git a/app/src/main/res/layout/component_two_button_dialog.xml b/app/src/main/res/layout/component_two_button_dialog.xml index 41578849f2d7a5bfd6de7537cb888dc11c278a44..3f4cb682564cdc8b23604c9d3ef74ac65b6e561f 100644 --- a/app/src/main/res/layout/component_two_button_dialog.xml +++ b/app/src/main/res/layout/component_two_button_dialog.xml @@ -7,7 +7,7 @@ <data> <variable name="ui" - type="io.xxlabs.messenger.ui.main.chats.TwoButtonInfoDialogUI" /> + type="io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI" /> </data> <androidx.cardview.widget.CardView diff --git a/app/src/main/res/layout/component_vertical_positive_negative_button.xml b/app/src/main/res/layout/component_vertical_positive_negative_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..ef876c6b036e13dc2c45e5cd2d80f6408b2fcec8 --- /dev/null +++ b/app/src/main/res/layout/component_vertical_positive_negative_button.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.ui.dialog.components.PositiveNegativeButtonUI" /> + </data> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <Button + android:id="@+id/positive_button" + style="@style/request_details_dialog_button" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="@dimen/registration_body_vertical_margin" + android:text="@{ui.positiveLabel == 0 ? android.R.string.ok : ui.positiveLabel}" + android:textColor="@color/neutral_white" + android:background="@drawable/bg_btn_white" + android:backgroundTint="@color/brand_default" + android:enabled="@{ui.positiveButtonEnabled}" + tools:text="Accept and Save"/> + + <Button + android:id="@+id/negative_button" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:text="@{ui.negativeLabel == 0 ? android.R.string.cancel : ui.negativeLabel}" + android:layout_marginBottom="50dp" + android:layout_marginHorizontal="@dimen/registration_horizontal_margin" + style="@style/request_details_borderless_text_button" + tools:text="Hide Request"/> + </LinearLayout> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_confirm_dialog.xml b/app/src/main/res/layout/component_warning_dialog.xml similarity index 96% rename from app/src/main/res/layout/component_confirm_dialog.xml rename to app/src/main/res/layout/component_warning_dialog.xml index 4a2fcd9192dc8b55c83c69b625cea438e78211c7..231eba08ee0c2d434c701f82c9f8c998c721830d 100644 --- a/app/src/main/res/layout/component_confirm_dialog.xml +++ b/app/src/main/res/layout/component_warning_dialog.xml @@ -7,7 +7,7 @@ <data> <variable name="ui" - type="io.xxlabs.messenger.support.dialog.confirm.ConfirmDialogUI" /> + type="io.xxlabs.messenger.ui.dialog.warning.WarningDialogUI" /> </data> <androidx.cardview.widget.CardView diff --git a/app/src/main/res/layout/fragment_backup_detail.xml b/app/src/main/res/layout/fragment_backup_detail.xml index 09e78a371eb28d4d124291edc5e263610a6946cd..cad90f55bc70e805dc53c68120ab12b379eed36b 100644 --- a/app/src/main/res/layout/fragment_backup_detail.xml +++ b/app/src/main/res/layout/fragment_backup_detail.xml @@ -3,12 +3,12 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" - tools:context=".backup.ui.save.BackupDetailFragment"> + tools:context=".backup.ui.backup.BackupDetailFragment"> <data> <variable name="ui" - type="io.xxlabs.messenger.backup.ui.save.BackupDetailUI" /> + type="io.xxlabs.messenger.backup.ui.backup.BackupDetailUI" /> </data> <androidx.constraintlayout.widget.ConstraintLayout @@ -37,7 +37,7 @@ style="@style/registration_step_next_button" android:layout_marginTop="24dp" android:enabled="@{ui.isEnabled}" - android:onClick="@{() -> ui.backup.backupNow()}" + android:onClick="@{() -> ui.backupNow()}" android:text="@string/backup_settings_backup_now" app:invisible="@{ui.backupInProgress}" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/fragment_backup_settings.xml b/app/src/main/res/layout/fragment_backup_settings.xml index e5a56d9d90284de7096f7ff4795b48283db3ba09..59f98b4ab33caf53f01c9265b9aeaa4b83b79668 100644 --- a/app/src/main/res/layout/fragment_backup_settings.xml +++ b/app/src/main/res/layout/fragment_backup_settings.xml @@ -3,12 +3,12 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" - tools:context=".backup.ui.save.BackupSettingsFragment"> + tools:context=".backup.ui.backup.BackupSettingsFragment"> <data> <variable name="ui" - type="io.xxlabs.messenger.backup.ui.save.BackupSettingsUI" /> + type="io.xxlabs.messenger.backup.ui.backup.BackupSettingsUI" /> </data> <androidx.constraintlayout.widget.ConstraintLayout @@ -37,7 +37,7 @@ style="@style/registration_step_next_button" android:layout_marginTop="24dp" android:enabled="@{ui.isEnabled}" - android:onClick="@{() -> ui.backup.backupNow()}" + android:onClick="@{() -> ui.backupNow()}" android:text="@string/backup_settings_backup_now" app:invisible="@{ui.backupInProgress}" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/fragment_request_list.xml b/app/src/main/res/layout/fragment_request_list.xml new file mode 100644 index 0000000000000000000000000000000000000000..b3060fe3c18241513d204457899c0f8f10aec407 --- /dev/null +++ b/app/src/main/res/layout/fragment_request_list.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + + </data> + + <FrameLayout + android:id="@+id/requestListLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".requests.ui.list.RequestListFragment"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/requestsListRV" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + </FrameLayout> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_requests.xml b/app/src/main/res/layout/fragment_requests.xml index d1fa95b81c40e2011cfa197ab84ff52c6b78ddf3..91412dc4132dd6dac7aeae4c34460d25c4d1d06e 100644 --- a/app/src/main/res/layout/fragment_requests.xml +++ b/app/src/main/res/layout/fragment_requests.xml @@ -32,49 +32,28 @@ android:background="@color/transparent" app:tabGravity="fill" app:tabIndicatorFullWidth="true" - app:tabIndicatorHeight="@dimen/spacing_5" - app:tabInlineLabel="true" + app:tabIndicatorHeight="@dimen/spacing_3" + app:tabInlineLabel="false" app:tabMaxWidth="0dp" app:tabMode="fixed" - app:tabPaddingBottom="@dimen/spacing_5" app:tabPaddingEnd="@dimen/spacing_24" app:tabPaddingStart="@dimen/spacing_24" app:tabTextAppearance="@style/TabTextStyle" + app:tabIconTint="@color/selector_qr_code_tab_secondary" app:tabTextColor="@color/selector_qr_code_tab_secondary" /> </com.google.android.material.appbar.AppBarLayout> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/requestsViewPager" - android:layout_width="0dp" + android:layout_width="match_parent" android:layout_height="0dp" android:orientation="horizontal" - android:paddingTop="@dimen/spacing_20" - android:paddingBottom="@dimen/spacing_20" + android:paddingHorizontal="@dimen/spacing_14" + android:paddingBottom="@dimen/spacing_14" app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/requestsAppBarLayout" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/requestsGuideStart" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_percent=".05" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/requestsGuideMiddle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_percent="0.50" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/requestsGuideEnd" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_percent=".95" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.core.widget.NestedScrollView> \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_backup_option.xml b/app/src/main/res/layout/list_item_backup_option.xml index 500875b12ce9216da5162443a2e19ae9abcc1187..1a9b040f4f8b2d6dfd646d49104077e64c126e91 100644 --- a/app/src/main/res/layout/list_item_backup_option.xml +++ b/app/src/main/res/layout/list_item_backup_option.xml @@ -7,7 +7,7 @@ <data> <variable name="ui" - type="io.xxlabs.messenger.backup.ui.save.SettingsOption" /> + type="io.xxlabs.messenger.backup.ui.backup.SettingsOption" /> </data> <androidx.constraintlayout.widget.ConstraintLayout diff --git a/app/src/main/res/layout/list_item_empty_placeholder.xml b/app/src/main/res/layout/list_item_empty_placeholder.xml new file mode 100644 index 0000000000000000000000000000000000000000..bda23e83f8e4d8c19deca9646b3a23ef4254e931 --- /dev/null +++ b/app/src/main/res/layout/list_item_empty_placeholder.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.list.adapter.RequestItem" /> + </data> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/no_results_placeholder" + style="@style/requests_list_empty_placeholder" + android:text="@{ui.title}" + tools:text="No recent requests received" /> + </LinearLayout> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_group_member.xml b/app/src/main/res/layout/list_item_group_member.xml new file mode 100644 index 0000000000000000000000000000000000000000..be5eec60da9779f57fd084e264cf2adfd2f70b3c --- /dev/null +++ b/app/src/main/res/layout/list_item_group_member.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.details.group.adapter.MemberItem" /> + </data> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp"> + + <include + layout="@layout/component_item_thumbnail" + android:id="@+id/thumbnail_layout" + app:ui="@{ui}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent"/> + + <TextView + android:id="@+id/member_name" + style="@style/request_item_header" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@{ui.name}" + app:layout_constraintStart_toEndOf="@id/thumbnail_layout" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/member_description" + app:layout_constraintEnd_toEndOf="parent" + tools:text="Almayra Zamzamy" /> + + <TextView + android:id="@+id/member_description" + style="@style/request_item_subheader" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@{ui.description}" + android:textColor="@{ui.descriptionTextColor}" + app:layout_constraintTop_toBottomOf="@id/member_name" + app:layout_constraintStart_toEndOf="@id/thumbnail_layout" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + tools:text="Creator" + tools:textColor="@color/accent_safe"/> + + </androidx.constraintlayout.widget.ConstraintLayout> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_hidden_requests_toggle.xml b/app/src/main/res/layout/list_item_hidden_requests_toggle.xml new file mode 100644 index 0000000000000000000000000000000000000000..0ad7f908748efb8eba33ba4e66c0ecef35058124 --- /dev/null +++ b/app/src/main/res/layout/list_item_hidden_requests_toggle.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <data> + <variable + name="listener" + type="io.xxlabs.messenger.requests.ui.list.adapter.ShowHiddenUI" /> + </data> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="16dp" + android:paddingBottom="24dp"> + + <View + android:id="@+id/divider" + android:layout_width="0dp" + android:layout_height="1dp" + android:background="@color/neutral_line" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + + <TextView + android:id="@+id/hidden_requests_label" + style="@style/requests_list_hidden_requests_label" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="Show hidden requests" + android:layout_marginTop="24dp" + app:layout_constraintTop_toBottomOf="@id/divider" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/hidden_requests_toggle" + app:layout_constraintBottom_toBottomOf="parent"/> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/hidden_requests_toggle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:onCheckedChanged="@{(_, bool)-> listener.onShowHiddenToggled(bool)}" + android:layout_marginTop="24dp" + app:layout_constraintTop_toBottomOf="@id/divider" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent"/> + </androidx.constraintlayout.widget.ConstraintLayout> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_invalid.xml b/app/src/main/res/layout/list_item_invalid.xml new file mode 100644 index 0000000000000000000000000000000000000000..a4af18e97ab83dfeb4c9eea47d6ad79649db269e --- /dev/null +++ b/app/src/main/res/layout/list_item_invalid.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <View + android:layout_width="match_parent" + android:layout_height="0dp"/> +</RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_request.xml b/app/src/main/res/layout/list_item_request.xml new file mode 100644 index 0000000000000000000000000000000000000000..69cf9407b7ebce4a790ce847f44162ff1ffe4247 --- /dev/null +++ b/app/src/main/res/layout/list_item_request.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <import type="io.xxlabs.messenger.ui.main.chat.DrawablePosition"/> + <variable + name="ui" + type="io.xxlabs.messenger.requests.ui.list.adapter.RequestItem" /> + <variable + name="listener" + type="io.xxlabs.messenger.requests.ui.list.adapter.RequestItemListener" /> + </data> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/requestItemLayout" + android:padding="16dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:onClick="@{() -> listener.onItemClicked(ui)}" > + + <include + layout="@layout/component_item_thumbnail" + android:id="@+id/requestProfilePhoto" + app:ui="@{ui}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/requestTitle" + style="@style/request_item_header" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{ui.title}" + app:layout_constraintEnd_toStartOf="@id/requestAction" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@id/requestProfilePhoto" + app:layout_constraintTop_toTopOf="@id/requestProfilePhoto" + tools:text="bartender007" /> + + <TextView + android:id="@+id/requestSubtitle" + style="@style/request_item_subheader" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{ui.subtitle}" + android:visibility="@{ui.subtitle}" + app:layout_constraintEnd_toStartOf="@id/requestAction" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@id/requestProfilePhoto" + app:layout_constraintTop_toBottomOf="@id/requestTitle" + tools:text="Bill Gates" /> + + <TextView + android:id="@+id/requestDetails" + style="@style/request_item_body" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:lineSpacingExtra="2sp" + android:text="@{ui.details}" + android:visibility="@{ui.details}" + app:layout_constraintEnd_toStartOf="@id/requestAction" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@id/requestProfilePhoto" + app:layout_constraintTop_toBottomOf="@id/requestSubtitle" + tools:text="+0 123-456-7890" /> + + <TextView + android:id="@+id/requestTimestamp" + style="@style/request_item_timestamp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:date="@{ui.timestamp}" + app:layout_constraintEnd_toStartOf="@id/requestAction" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@id/requestProfilePhoto" + app:layout_constraintTop_toBottomOf="@id/requestDetails" + tools:text="5 minutes ago" /> + + <TextView + android:id="@+id/requestAction" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{ui.actionLabel}" + android:padding="12dp" + android:layout_marginHorizontal="0dp" + android:drawablePadding="4dp" + android:onClick="@{() -> listener.onActionClicked(ui)}" + android:visibility="@{ui.actionLabel}" + android:singleLine="false" + android:gravity="center_vertical|end" + app:actionIcon="@{ui.actionIcon}" + app:iconColor="@{ui.actionIconColor}" + app:iconPosition="@{DrawablePosition.START}" + app:customStyle="@{ui.actionTextStyle}" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + tools:drawableLeft="@drawable/ic_check_green" + tools:text="Verified" + style="@style/request_item_subheader"/> + </androidx.constraintlayout.widget.ConstraintLayout> +</layout> \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_requests.xml b/app/src/main/res/layout/list_item_requests.xml index b7e441ad9e6ec3054989234dac69a797cf45b84a..baf09c9677baee7a79607176ea16ef0b39b17960 100644 --- a/app/src/main/res/layout/list_item_requests.xml +++ b/app/src/main/res/layout/list_item_requests.xml @@ -79,7 +79,7 @@ android:maxLines="1" android:textColor="@color/neutral_weak" android:textSize="@dimen/text_10" - tools:text="2 hours ago" /> + tools:text="2 hours ago"/> </LinearLayout> <LinearLayout @@ -98,7 +98,8 @@ android:gravity="end" android:textColor="@color/neutral_weak" android:textSize="@dimen/text_10" - tools:text="2 hours ago" /> + tools:text="2 hours ago" + tools:visibility="gone"/> <TextView android:id="@+id/itemRequestsResend" diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index fb57bdc51dd8e98b076d73db91f90b3f4169842d..e26dbad80345c2c6744e196b898c34ca050a1d5e 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -133,6 +133,16 @@ app:popExitAnim="@anim/slide_out_right" app:popUpTo="@id/chatsFragment" app:popUpToInclusive="true" /> + <argument + android:name="groupId" + app:argType="string" + app:nullable="true" + android:defaultValue="@null"/> + <argument + android:name="group" + app:argType="io.xxlabs.messenger.data.room.model.GroupData" + app:nullable="true" + android:defaultValue="@null"/> </fragment> <fragment @@ -202,6 +212,17 @@ app:popExitAnim="@anim/slide_out_right" app:popUpTo="@id/chatsFragment" app:popUpToInclusive="true" /> + + <argument + android:name="contactId" + app:argType="string" + app:nullable="true" + android:defaultValue="@null"/> + <argument + android:name="contact" + app:argType="io.xxlabs.messenger.data.room.model.ContactData" + app:nullable="true" + android:defaultValue="@null"/> </fragment> <fragment @@ -291,7 +312,7 @@ <fragment android:id="@+id/requestsFragment" - android:name="io.xxlabs.messenger.ui.main.requests.RequestsFragment" + android:name="io.xxlabs.messenger.requests.ui.RequestsFragment" android:label="RequestsFragment" tools:layout="@layout/fragment_requests"> @@ -300,6 +321,12 @@ android:defaultValue="0" app:argType="integer" /> + <action + android:id="@+id/action_requests_to_privateMessages" + app:destination="@id/chatFragment" /> + <action + android:id="@+id/action_requests_to_groupMessages" + app:destination="@id/groupsChatFragment" /> <action android:id="@+id/action_requests_to_ud_search" app:destination="@id/udPrivateSearchFragment" @@ -431,12 +458,12 @@ <fragment android:id="@+id/backupDetailFragment" - android:name="io.xxlabs.messenger.backup.ui.save.BackupDetailFragment" + android:name="io.xxlabs.messenger.backup.ui.backup.BackupDetailFragment" android:label="fragment_backup_settings" tools:layout="@layout/fragment_backup_detail" > <argument - android:name="backupOption" - app:argType="io.xxlabs.messenger.backup.model.BackupOption" /> + android:name="source" + app:argType="io.xxlabs.messenger.backup.data.BackupSource" /> </fragment> <fragment android:id="@+id/backupListFragment" @@ -451,12 +478,17 @@ </fragment> <fragment android:id="@+id/backupSettingsFragment" - android:name="io.xxlabs.messenger.backup.ui.save.BackupSettingsFragment" + android:name="io.xxlabs.messenger.backup.ui.backup.BackupSettingsFragment" android:label="fragment_backup_settings" tools:layout="@layout/fragment_backup_settings" > <action android:id="@+id/action_backup_settings_to_backup_detail" app:destination="@id/backupDetailFragment" /> </fragment> + <fragment + android:id="@+id/receivedRequestsFragment" + android:name="io.xxlabs.messenger.requests.ui.list.ReceivedRequestsFragment" + android:label="ReceivedRequestsFragment" > + </fragment> </navigation> \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_registration.xml b/app/src/main/res/navigation/nav_registration.xml index ca713af608eb854677494186ef4f31fa2ac30549..5b284558ed4b2531e97bf35e8f5aa52ce357987e 100644 --- a/app/src/main/res/navigation/nav_registration.xml +++ b/app/src/main/res/navigation/nav_registration.xml @@ -97,8 +97,8 @@ android:label="fragment_backup_found" tools:layout="@layout/fragment_restore_detail" > <argument - android:name="restoreOption" - app:argType="io.xxlabs.messenger.backup.model.RestoreOption" /> + android:name="source" + app:argType="io.xxlabs.messenger.backup.data.BackupSource" /> <action android:id="@+id/actionRestoreDetailToRegistrationCompleted" app:destination="@id/registrationCompletedStepFragment" /> diff --git a/app/src/main/res/values-v28/styles.xml b/app/src/main/res/values-v28/styles.xml new file mode 100644 index 0000000000000000000000000000000000000000..a33bb3cf74ccba46342b5e0235ae4ebcb645d957 --- /dev/null +++ b/app/src/main/res/values-v28/styles.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="request_details_dialog_subtitle" parent="request_details_dialog_title"> + <item name="android:textSize">26sp</item> + <item name="android:textAllCaps">false</item> + <item name="android:textStyle">bold</item> + <item name="android:fontWeight">800</item> + <item name="android:lineHeight">32.63sp</item> + </style> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index b64c881afe9c8758c6d070d865c84817f263654f..da985b1aff4d14d0b2b483422863680f162eb439 100755 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -139,7 +139,7 @@ <!-- Chats --> <color name="chatsListTitleColor">#696969</color> <color name="chatsPreviewColor">#70828f</color> - <color name="chatsAccentColor">#037281</color> + <color name="chatsAccentColor">@color/brand_default</color> <color name="chatBgColorSent">#008292</color> <color name="chatBgColorError">#9dbec1</color> <color name="chatReplyBgColorLight">#eff9fa</color> @@ -176,6 +176,7 @@ <!-- Neutral --> <color name="neutral_active">#242424</color> + <color name="neutral_secondary">#727578</color> <color name="neutral_dark">#373737</color> <color name="neutral_body">#3D3D3D</color> <color name="neutral_weak">#A4A4A4</color> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eae63ff153e11ce3bb2fd2495fe10bc39adf1cdd..7139d9aa1a2c94709cd6632c4604f53355bca0f4 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -404,7 +404,7 @@ @string/backup_setup_description_span_text</string> <string name="backup_settings_backup_now">Backup Now</string> <string name="backup_encryption_warning"> - Content backed up in %s is not protected by xx Network end-to-end encryption.</string> + Content backed up in %s is encrypted with your passphrase in a brute force resistant manner.</string> <string name="backup_latest_backup_label">Latest backup</string> <string name="backup_latest_backup_never">Never</string> <string name="backup_frequency_label">Backup to %s</string> @@ -472,7 +472,7 @@ <string name="title_activity_intro">IntroActivity</string> <string name="hello_first_fragment">Hello first fragment</string> <string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string> - <string name="contact_details_nickname">Nickname</string> + <string name="contact_details_nickname">Nick name</string> <string name="menu_join_xx_network">Join xx network</string> <string name="profile_none_provided">None provided</string> <string name="contacts_max_members_msg">You have reached the maximum amount of members.</string> @@ -553,4 +553,39 @@ <string name="backup_restore_password_restore_title">Backup password</string> <string name="backup_restore_password_restore_hint">Enter password</string> <string name="backup_restore_encrypting_backup_text">Initializing and securing your backup file will take few seconds, please keep the app open</string> + <string name="request_details_title">Request from</string> + <string name="request_details_email_header">Email address</string> + <string name="request_details_phone_header">Phone number</string> + <string name="request_details_nickname_section">Edit your new contact\'s nick name.</string> + <string name="group_invitation_title">Group chat request</string> + <string name="request_details_positive_button">Accept and save</string> + <string name="request_details_negative_button">Hide request</string> + <string name="request_accepted_title">New Connection</string> + <string name="request_accepted_body">Is now a connection, would you like to send a message?</string> + <string name="contact_accepted_positive_button">Send a message</string> + <string name="contact_accepted_negative_button">Later</string> + <string name="invitation_details_positive_button">Accept</string> + <string name="invitation_details_negative_button">Hide request</string> + <string name="group_member_creator">Creator</string> + <string name="group_member_not_connection">Not a connection</string> + <string name="group_member_is_connection">Is a connection</string> + <string name="invitation_accepted_title">Accepted</string> + <string name="invitation_accepted_body">You are now part of the group chat. Would you like to check it out?</string> + <string name="invitation_accepted_positive_button">Go to chat</string> + <string name="requests_empty_placeholder_failed">No recent failed requests</string> + <string name="requests_empty_placeholder_sent">No recent sent requests</string> + <string name="requests_empty_placeholder_received">No recent requests received</string> + <string name="requests_empty_placeholder_hidden">No recent hidden requests</string> + <string name="send_request_positive_button">Send Contact Request</string> + <string name="send_request_negative_button">Cancel</string> + <string name="send_request_body">"Share your information with <annotation tag="receiverName">%1$s</annotation><annotation tag="receiverFact">%2$s</annotation>so they know it’s you.</string> + <string name="send_request_dialog_title">Request Contact</string> + <string name="send_request_fact_placeholder">@string/profile_none_provided</string> + <string name="save_nickname_title">Add a nickname</string> + <string name="save_nickname_positive_button">Save</string> + <string name="save_nickname_dialog_body">Edit your new contact’s nickname so you know who they are.</string> + <string name="request_item_action_retry">Resend</string> + <string name="request_item_action_resent">Resent</string> + <string name="request_item_action_verifying">Verifying</string> + <string name="request_item_action_failed_verification">Failed to\nverify</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 421fcb792ab630a17c928ff6d3e1b359f88fb022..b951251e0d4f943b5532e752f76a60758f0472af 100755 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -4,7 +4,7 @@ <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar.Bridge"> <item name="colorPrimary">@color/brand_default</item> <item name="colorPrimaryDark">@color/brand_dark</item> - <item name="colorAccent">@color/brand_dark</item> + <item name="colorAccent">@color/brand_light</item> <item name="android:windowBackground">@color/neutral_white</item> <item name="android:textAppearance">@style/XxTextStyle</item> <item name="android:homeAsUpIndicator">@drawable/ic_back</item> @@ -168,7 +168,7 @@ <item name="android:fontFamily">@font/gotham_book</item> <item name="android:textStyle">italic</item> <item name="lineHeight">@dimen/spacing_18</item> - <item name="android:textColor">@color/tealThemeColor</item> + <item name="android:textColor">@color/brand_default</item> </style> <style name="XxInputHintAppearance" parent="TextAppearance.Design.Hint"> @@ -569,6 +569,7 @@ <item name="android:textAllCaps">false</item> </style> + <!-- xx_button_filled --> <style name="confirm_dialog_button" parent="dialog_button"> <item name="android:layout_width">0dp</item> @@ -584,7 +585,6 @@ <item name="android:layout_marginBottom">@dimen/registration_vertical_margin</item> <item name="android:textAllCaps">false</item> <item name="android:fontFamily">@font/mulish_regular</item> - <item name="fontFamily">@font/mulish_regular</item> </style> <style name="info_tooltip_textview" parent="XxTextStyle.SemiBold"> @@ -625,4 +625,140 @@ <item name="android:layout_height">match_parent</item> <item name="android:background">@color/background</item> </style> + + <style name="request_item_text"> + <item name="android:layout_marginTop">4dp</item> + <item name="android:layout_marginHorizontal">16dp</item> + <item name="android:singleLine">true</item> + <item name="android:textSize">14sp</item> + <item name="android:fontFamily">@font/mulish_regular</item> + </style> + + <style name="request_item_header" parent="request_item_text"> + <item name="android:layout_marginTop">0dp</item> + <item name="android:textColor">@color/neutral_active</item> + <item name="android:fontFamily">@font/mulish_semi_bold</item> + </style> + + <style name="request_item_subheader" parent="request_item_text"> + <item name="android:textColor">@color/neutral_secondary</item> + </style> + + <style name="request_item_body" parent="request_item_text"> + <item name="android:textColor">@color/neutral_secondary</item> + <item name="android:singleLine">false</item> + </style> + + <style name="request_item_timestamp" parent="request_item_text"> + <item name="android:textSize">12sp</item> + <item name="android:textColor">@color/neutral_weak</item> + </style> + + <style name="request_item_action" parent="request_item_text"> + <item name="android:fontFamily">@font/mulish_semi_bold</item> + </style> + + <style name="request_item_error" parent="request_item_action"> + <item name="android:textColor">@color/accent_danger</item> + </style> + + <style name="request_item_retry" parent="request_item_action"> + <item name="android:textColor">@color/brand_default</item> + </style> + + <style name="request_item_resent" parent="request_item_action"> + <item name="android:textColor">@color/neutral_weak</item> + </style> + + <style name="request_item_verifying" parent="request_item_resent"/> + + <style name="request_details_dialog_title" parent="dialog_title"> + <item name="android:layout_marginTop">0dp</item> + <item name="android:textSize">12sp</item> + <item name="android:textAllCaps">true</item> + <item name="android:fontWeight">700</item> + </style> + + <style name="request_details_dialog_subtitle" parent="request_details_dialog_title"> + <item name="android:textSize">26sp</item> + <item name="android:textAllCaps">false</item> + <item name="android:textStyle">bold</item> + <item name="android:fontWeight">800</item> + </style> + + <style name="request_details_section_header" parent="request_details_dialog_title"> + <item name="android:textColor">@color/neutral_weak</item> + </style> + + <style name="request_details_section_content" parent="request_details_section_header"> + <item name="android:textSize">14sp</item> + <item name="android:textAllCaps">false</item> + <item name="android:textColor">@color/neutral_active</item> + </style> + + <style name="request_details_textinput_label" parent="request_details_section_header"> + <item name="android:textSize">15sp</item> + <item name="android:textAllCaps">false</item> + <item name="android:textColor">@color/neutral_active</item> + <item name="android:fontFamily">@font/mulish_semi_bold</item> + <item name="android:fontWeight">700</item> + </style> + + <style name="request_details_dialog_button" parent="dialog_button"> + <item name="android:textSize">16sp</item> + <item name="fontFamily">@font/mulish_semi_bold</item> + </style> + + <style name="request_details_borderless_text_button" parent="registration_borderless_text_button"> + <item name="android:textSize">16sp</item> + <item name="fontFamily">@font/mulish_semi_bold</item> + </style> + + <style name="invitation_details_members_list"> + <item name="android:layout_marginStart">@dimen/registration_horizontal_margin</item> + <item name="android:layout_marginEnd">@dimen/registration_horizontal_margin</item> + <item name="android:layout_marginBottom">24dp</item> + </style> + + <style name="requests_list_empty_placeholder" parent="xx_font"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">125dp</item> + <item name="android:textSize">14sp</item> + <item name="android:textColor">@color/neutral_weak</item> + <item name="android:fontWeight">400</item> + <item name="android:gravity">center</item> + </style> + + <style name="requests_list_hidden_requests_label" parent="XxTextStyle.SemiBold"> + <item name="android:textSize">16sp</item> + <item name="android:textColor">@color/neutral_active</item> + <item name="android:fontWeight">600</item> + <item name="android:gravity">center_vertical</item> + </style> + + <style name="custom_toast_text" parent="xx_font"> + <item name="android:textColor">@color/neutral_white</item> + <item name="android:letterSpacing">0.03</item> + <item name="android:fontWeight">600</item> + </style> + + <style name="custom_toast_header" parent="custom_toast_text"> + <item name="android:textSize">16sp</item> + <item name="android:lineSpacingExtra">8sp</item> + </style> + + <style name="custom_toast_body" parent="custom_toast_text"> + <item name="android:lineSpacingExtra">4sp</item> + <item name="android:textSize">14sp</item> + </style> + + <style name="toast_action_button" parent="custom_toast_text"> + <item name="android:textStyle">bold</item> + <item name="android:textAllCaps">true</item> + <item name="android:fontWeight">700</item> + <item name="android:textSize">12sp</item> + <item name="android:letterSpacing">0.05</item> + <item name="android:lineSpacingExtra">3sp</item> + <item name="android:gravity">center</item> + </style> </resources> diff --git a/app/src/test/java/io/xxlabs/messenger/backup/ui/save/BackupPasswordTest.kt b/app/src/test/java/io/xxlabs/messenger/backup/ui/backup/BackupPasswordTest.kt similarity index 97% rename from app/src/test/java/io/xxlabs/messenger/backup/ui/save/BackupPasswordTest.kt rename to app/src/test/java/io/xxlabs/messenger/backup/ui/backup/BackupPasswordTest.kt index 8b6b92454c552fb9c7d10eb72d099f743e6c85dd..32c07ae0f3140a18512de3f452b3f9dda1430af0 100644 --- a/app/src/test/java/io/xxlabs/messenger/backup/ui/save/BackupPasswordTest.kt +++ b/app/src/test/java/io/xxlabs/messenger/backup/ui/backup/BackupPasswordTest.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.backup.ui.save +package io.xxlabs.messenger.backup.ui.backup import com.google.common.truth.Truth import io.xxlabs.messenger.randomCaps diff --git a/app/src/test/java/io/xxlabs/messenger/ui/main/requests/RequestsViewModelTest.kt b/app/src/test/java/io/xxlabs/messenger/requests/RequestsViewModelTest.kt similarity index 99% rename from app/src/test/java/io/xxlabs/messenger/ui/main/requests/RequestsViewModelTest.kt rename to app/src/test/java/io/xxlabs/messenger/requests/RequestsViewModelTest.kt index ec81d88acb486f18774725c133fea0aedc1e9ce6..ecdf3bb1483adce13c52030c6a4b38797fffef8f 100644 --- a/app/src/test/java/io/xxlabs/messenger/ui/main/requests/RequestsViewModelTest.kt +++ b/app/src/test/java/io/xxlabs/messenger/requests/RequestsViewModelTest.kt @@ -1,4 +1,4 @@ -package io.xxlabs.messenger.ui.main.requests +package io.xxlabs.messenger.requests // //import android.content.Context //import com.google.common.truth.Truth.assertThat diff --git a/build.gradle.kts b/build.gradle.kts index be2a848fbf2e2760fc406f94e4a2dcec41f4d9d3..407cded3f9bcf20b8f7c05b602f16bce352232fd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,7 @@ buildscript { classpath("org.jacoco:org.jacoco.core:0.8.7") classpath("com.android.tools.build:gradle:7.1.2") classpath("com.google.protobuf:protobuf-gradle-plugin:0.8.18") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21") classpath("com.google.gms:google-services:4.3.10") classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1") classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.4.1") diff --git a/xx_bindings/bindings.aar b/xx_bindings/bindings.aar index 5792eaf4f72489a503edbec75be73ca379c3090f..3946420668a55b90f772ccd8d9379567d586bb5e 100644 Binary files a/xx_bindings/bindings.aar and b/xx_bindings/bindings.aar differ