Track client and server sources

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 12:04:01 +03:00
parent d9f7603ae8
commit de9dd05308
383 changed files with 44782 additions and 2 deletions

Submodule messenger-client deleted from 003168a95e

27
messenger-client/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Android Studio
.idea/
.gradle/
build/
app/build/
app/release/
local.properties
.DS_Store
Thumbs.db
*.iml
*.iws
*.ipr
*.bak
# Firebase
app/google-services.json
# Keystore files
*.jks
*.keystore
app/.cxx/
captures/
# Additional files
*.log

201
messenger-client/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,142 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
fun String.asBuildConfigString(): String {
return "\"${replace("\\", "\\\\").replace("\"", "\\\"")}\""
}
val serverUrl = providers.gradleProperty("SERVER_URL")
.orElse(providers.environmentVariable("SERVER_URL"))
.orElse("https://msgr.jeezft.xyz")
.get()
.trimEnd('/') + "/"
val webSocketUrl = providers.gradleProperty("WEB_SOCKET_URL")
.orElse(providers.environmentVariable("WEB_SOCKET_URL"))
.orElse("wss://msgr.jeezft.xyz")
.get()
.trimEnd('/')
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.gms)
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.21"
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}
android {
namespace = "com.aiwazian.messenger"
compileSdk = 36
defaultConfig {
applicationId = "com.aiwazian.messenger"
minSdk = 28
targetSdk = 36
versionCode = 15
versionName = "1.7.0"
buildConfigField("String", "SERVER_URL", serverUrl.asBuildConfigString())
buildConfigField("String", "WEB_SOCKET_URL", webSocketUrl.asBuildConfigString())
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
viewBinding = true
buildConfig = true
}
buildToolsVersion = "36.0.0"
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
}
dependencies {
// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging)
implementation(libs.firebase.analytics)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.animation)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended.android)
implementation(libs.androidx.foundation)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
// DataStore
implementation(libs.androidx.datastore.preferences)
implementation(libs.accompanist.systemuicontroller)
implementation(libs.accompanist.navigation.material)
implementation(libs.accompanist.navigation.animation)
implementation(libs.protobuf.javalite)
implementation(libs.retrofit)
implementation(libs.converter.gson)
implementation(libs.kotlinx.serialization.json)
// Ktor
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.websockets)
implementation(libs.coil.compose)
// Lottie animation
implementation(libs.lottie.compose)
implementation(libs.zxing.android.embedded)
implementation(libs.okhttp)
implementation(libs.socket.io.client)
// Dagger Hilt
implementation(libs.hilt.android)
implementation(libs.androidx.graphics.shapes)
debugImplementation(libs.androidx.compose.ui.tooling)
ksp(libs.hilt.compiler)
implementation(libs.androidx.hilt.navigation.compose)
// Room database
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.material.icons.extended)
}

27
messenger-client/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,27 @@
-keepattributes Signature
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
# R8 full mode strips generic signatures from return types if not kept.
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
# With R8 full mode generic signatures are stripped for classes that are not kept.
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# Deleting all calls Log.v, Log.d, Log.i, Log.w, Log.e, Log.wtf
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** e(...);
public static *** i(...);
public static *** v(...);
public static *** w(...);
public static *** wtf(...);
}

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission
android:name="android.permission.CAMERA"
tools:node="remove" />
<application
android:name=".Application"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/new_app_icon"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/new_app_icon"
android:supportsRtl="true"
android:theme="@style/Theme.Messenger"
android:usesCleartextTraffic="true"
tools:ignore="DataExtractionRules"
tools:targetApi="33">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.aiwazian.messenger.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".services.NotificationService"
android:exported="false"
android:launchMode="singleTop">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity
android:name=".LoginActivity"
android:exported="false"
android:theme="@style/Theme.Messenger" />
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:exported="true"
android:theme="@style/Theme.Messenger">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
package com.aiwazian.messenger
import android.app.Application
import com.google.firebase.FirebaseApp
import dagger.hilt.android.HiltAndroidApp
import java.util.Locale
@HiltAndroidApp
class Application : Application() {
override fun onCreate() {
super.onCreate()
Locale.setDefault(Locale.forLanguageTag("ru"))
FirebaseApp.initializeApp(this)
}
}

View File

@@ -0,0 +1,25 @@
package com.aiwazian.messenger
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.aiwazian.messenger.ui.login.AuthScreen
import com.aiwazian.messenger.ui.theme.ApplicationTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ApplicationTheme(dynamicColor = true) {
AuthScreen()
}
}
}
}

View File

@@ -0,0 +1,190 @@
package com.aiwazian.messenger
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiwazian.messenger.database.repository.UserRepository
import com.aiwazian.messenger.services.AppLockService
import com.aiwazian.messenger.services.DataStoreManager
import com.aiwazian.messenger.services.NotificationService
import com.aiwazian.messenger.services.ThemeService
import com.aiwazian.messenger.services.TokenManager
import com.aiwazian.messenger.services.UserManager
import com.aiwazian.messenger.ui.ChatScreen
import com.aiwazian.messenger.ui.LockScreen
import com.aiwazian.messenger.ui.MainScreen
import com.aiwazian.messenger.ui.element.NavigationController
import com.aiwazian.messenger.ui.theme.ApplicationTheme
import com.aiwazian.messenger.utils.ChatState
import com.aiwazian.messenger.utils.WebSocketManager
import com.aiwazian.messenger.viewModels.NavigationViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var appLockService: AppLockService
@Inject
lateinit var themeService: ThemeService
@Inject
lateinit var userRepository: UserRepository
lateinit var navViewModel: NavigationViewModel
override fun attachBaseContext(newBase: Context) {
DataStoreManager.initialize(newBase)
runBlocking { TokenManager.init() }
super.attachBaseContext(newBase)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (TokenManager.getToken().isBlank()) {
startActivity(
Intent(
this,
LoginActivity::class.java
).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
)
finish()
return
}
enableEdgeToEdge()
TokenManager.setUnauthorizedCallback {
val intent = Intent(
this,
LoginActivity::class.java
).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
this@MainActivity.startActivity(intent)
this@MainActivity.finish()
}
setContent {
val isLockApp by appLockService.isLockApp.collectAsState()
val selectedTheme by themeService.currentTheme.collectAsState()
val selectedColor by themeService.primaryColor.collectAsState()
val isDynamicColorEnable by themeService.dynamicColor.collectAsState()
LaunchedEffect(Unit) {
try {
WebSocketManager.onConnect = {
lifecycleScope.launch {
UserManager.loadUserData(userRepository)
}
}
WebSocketManager.onClose = { code ->
if (code == 1008) {
TokenManager.getUnauthorizedCallback()?.invoke()
} else {
lifecycleScope.launch {
delay(1000)
WebSocketManager.connect()
}
}
}
WebSocketManager.onFailure = {
lifecycleScope.launch {
delay(1000)
WebSocketManager.connect()
}
}
WebSocketManager.connect()
UserManager.loadUserData(userRepository)
} catch (e: Exception) {
Log.e(
"MainActivity",
"Ошибка подключения вебсокета",
e
)
}
try {
val notificationService = NotificationService()
val token = notificationService.getFirebaseToken()
notificationService.sendTokenToServer(token)
} catch (e: Exception) {
Log.e(
"MainActivity",
"Ошибка при отправке токена для уведомлений на сервер",
e
)
}
}
ApplicationTheme(
theme = selectedTheme,
dynamicColor = isDynamicColorEnable,
primaryColor = selectedColor.color
) {
navViewModel = viewModel<NavigationViewModel>()
NavigationController {
MainScreen()
}
AnimatedVisibility(
visible = isLockApp,
enter = fadeIn(tween(100)),
exit = fadeOut(tween(100))
) {
LockScreen()
}
LaunchedEffect(Unit) {
val chatId = intent.getStringExtra("chatId")?.toLongOrNull()
if (chatId != null && !ChatState.isChatOpen(chatId)) {
navViewModel.addScreenInStack {
ChatScreen(chatId)
}
}
}
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val chatId = intent.getStringExtra("chatId")?.toLongOrNull()
if (chatId != null) {
if (!ChatState.isChatOpen(chatId)) {
navViewModel.addScreenInStack {
ChatScreen(chatId)
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
package com.aiwazian.messenger.api
import com.aiwazian.messenger.services.TokenManager
import okhttp3.Interceptor
import okhttp3.Response
class AuthInterceptor(
private val getToken: () -> String?,
private val shouldSkipAuth: (String) -> Boolean,
private val onUnauthorized: (() -> Unit)?
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val path = request.url.encodedPath
if (shouldSkipAuth(path)) {
return chain.proceed(request)
}
val token = getToken()
val authRequest = if (!token.isNullOrEmpty()) {
request.newBuilder()
.addHeader(
"Authorization",
"Bearer $token"
)
.build()
} else {
request
}
val response = chain.proceed(authRequest)
if (response.code == 401 && TokenManager.isAuthorized()) {
TokenManager.setAuthorized(false)
onUnauthorized?.invoke()
}
return response
}
}

View File

@@ -0,0 +1,25 @@
package com.aiwazian.messenger.api
import com.aiwazian.messenger.utils.ProgressResponseBody
import okhttp3.Interceptor
import okhttp3.Response
class ProgressInterceptor(
private val onProgress: (url: String, progress: Int) -> Unit
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val url = request.url.toString()
val body = response.body
return response.newBuilder()
.body(
ProgressResponseBody(
url,
body,
onProgress
)
)
.build()
}
}

View File

@@ -0,0 +1,85 @@
package com.aiwazian.messenger.api
import com.aiwazian.messenger.interfaces.ApiService
import com.aiwazian.messenger.services.TokenManager
import com.aiwazian.messenger.utils.Constants
import com.aiwazian.messenger.utils.DownloadManager
import com.aiwazian.messenger.utils.Route
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.lang.reflect.Type
import java.util.concurrent.TimeUnit
private object LongTypeAdapter : JsonDeserializer<Long>, JsonSerializer<Long> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Long {
return when {
json.isJsonPrimitive && json.asJsonPrimitive.isNumber -> json.asLong
json.isJsonPrimitive -> json.asString.toLongOrNull() ?: 0L
else -> 0L
}
}
override fun serialize(src: Long, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonPrimitive(src)
}
}
object RetrofitInstance {
private val gson = GsonBuilder()
.registerTypeAdapter(Long::class.java, LongTypeAdapter)
.registerTypeAdapter(Long::class.javaObjectType, LongTypeAdapter)
.create()
private val BASE_URL = Constants.SERVER_URL
private val skipAuthPaths = listOf(
Route.LOGIN,
Route.REGISTER,
Route.FIND_USER_BY_LOGIN
)
private val okHttpClient = OkHttpClient.Builder().connectTimeout(
1,
TimeUnit.MINUTES
).readTimeout(
1,
TimeUnit.MINUTES
).writeTimeout(
1,
TimeUnit.MINUTES
).addInterceptor(
AuthInterceptor(
getToken = {
TokenManager.getToken()
},
shouldSkipAuth = { path ->
skipAuthPaths.contains(path)
},
onUnauthorized = {
TokenManager.getUnauthorizedCallback()?.invoke()
})
).addNetworkInterceptor(
ProgressInterceptor { url, progress ->
DownloadManager.updateProgress(
url,
progress
)
}).build()
val api: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(ApiService::class.java)
}
}

View File

@@ -0,0 +1,14 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
@Keep
data class ApiResponse(
@Keep val ok: Boolean,
@Keep val message: String
)
@Keep
data class UsernameAvailability(
@Keep val available: Boolean = false
)

View File

@@ -0,0 +1,34 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import kotlinx.serialization.Serializable
import com.google.gson.annotations.SerializedName
@Keep
@Serializable
data class Attachment(
@Keep val id: String,
@Keep val messageId: Int = 0,
@Keep val chatId: Long = 0,
@Keep val name: String,
@Keep val url: String = "",
@Keep val size: Long,
@Keep val mimeType: String? = null
)
@Keep
data class FileDownloadUrlResponse(
val downloadUrl: String,
val name: String,
val size: String,
val mimeType: String
)
@Keep
data class FileUploadInitRequest(val name: String, val size: Long, val mimeType: String)
@Keep
data class FileUploadInitResponse(val signedUrl: String, val fileId: String)
@Keep
data class FileUploadConfirmRequest(val fileId: String, val text: String? = null)

View File

@@ -0,0 +1,12 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
@Keep
data class AuthRequest(
@Keep val login: String,
@Keep val password: String,
@Keep val deviceModel: String,
@Keep val osVersion: String,
@Keep val osName: String
)

View File

@@ -0,0 +1,9 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
@Keep
data class AuthResponse(
@Keep val token: String,
@Keep val userId: String
)

View File

@@ -0,0 +1,5 @@
package com.aiwazian.messenger.data
data class ChangeCloudPasswordRequest(
val password: String
)

View File

@@ -0,0 +1,30 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import com.aiwazian.messenger.enums.ChannelType
import com.aiwazian.messenger.interfaces.Profile
import com.google.gson.annotations.SerializedName
@Keep
data class ChannelInfo(
@Keep override val id: Long = 0,
@Keep val ownerId: Long = 0,
@Keep val name: String = "",
@Keep val bio: String = "",
@Keep val subscribers: Int = 0,
@Keep val removedUser: Int = 0,
@Keep var channelType: Int = ChannelType.PRIVATE.ordinal,
@Keep var publicLink: String? = null,
@Keep val isSubscribed: Boolean = false
) : Profile
@Keep
data class CreateChannelRequest(
@SerializedName("name") val name: String,
@SerializedName("bio") val bio: String? = null,
@SerializedName("channelType") val channelType: String,
@SerializedName("username") val username: String? = null
)
@Keep
data class CreatedEntityResponse(@SerializedName("id") val id: Long)

View File

@@ -0,0 +1,16 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import com.aiwazian.messenger.enums.ChatType
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
@Keep
@Serializable
data class ChatInfo(
@Keep var id: Long = 0,
@Keep @SerializedName("name") var chatName: String = "",
@Keep var isPinned: Boolean = false,
@Keep var lastMessage: Message? = null
)

View File

@@ -0,0 +1,16 @@
package com.aiwazian.messenger.data
import androidx.compose.ui.graphics.Color
data class CustomColors(
val secondary: Color,
val background: Color,
var primary: Color,
val text: Color,
val textHint: Color,
val topAppBarBackground: Color,
val sendMessageTimeBackground: Color,
val danger: Color,
val dangerBackground: Color,
val horizontalDivider: Color
)

View File

@@ -0,0 +1,8 @@
package com.aiwazian.messenger.data
import kotlinx.serialization.Serializable
@Serializable
data class DeleteChatPayload (
val chatId: Long
)

View File

@@ -0,0 +1,9 @@
package com.aiwazian.messenger.data
import kotlinx.serialization.Serializable
@Serializable
data class DeleteMessagePayload(
val chatId: Long,
val messageId: Int
)

View File

@@ -0,0 +1,15 @@
package com.aiwazian.messenger.data
import com.aiwazian.messenger.enums.DownloadStatus
import okhttp3.ResponseBody
import retrofit2.Call
data class DownloadItem(
val fileId: String = "",
val url: String,
val fileName: String,
var progress: Int = 0,
var status: DownloadStatus = DownloadStatus.PENDING,
var call: Call<ResponseBody>? = null,
var onComplete: (() -> Unit)? = null
)

View File

@@ -0,0 +1,9 @@
package com.aiwazian.messenger.data
import androidx.compose.ui.graphics.vector.ImageVector
data class DropdownMenuAction(
val icon: ImageVector,
val text: String,
val onClick: () -> Unit
)

View File

@@ -0,0 +1,10 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
@Keep
data class FolderInfo(
@Keep var id: Int = 0,
@Keep var name: String = "",
@Keep var chats: List<ChatInfo> = emptyList()
)

View File

@@ -0,0 +1,20 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import com.aiwazian.messenger.interfaces.Profile
import com.google.gson.annotations.SerializedName
@Keep
data class GroupInfo(
@Keep override val id: Long = 0,
@Keep val ownerId: Long = 0,
@Keep val name: String = "",
@Keep val bio: String = "",
@Keep val members: Int = 0
) : Profile
@Keep
data class CreateGroupRequest(
@SerializedName("name") val name: String,
@SerializedName("bio") val bio: String? = null
)

View File

@@ -0,0 +1,8 @@
package com.aiwazian.messenger.data
import kotlinx.serialization.Serializable
@Serializable
data class HistoryClearPayload(
val chatId: String
)

View File

@@ -0,0 +1,60 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Keep
@Serializable
data class KanbanBoard(
val id: Int = 0,
val title: String = "",
val columns: List<KanbanColumn> = emptyList(),
val updatedAt: Long = 0
)
@Keep
@Serializable
data class KanbanColumn(
val id: Int = 0,
val boardId: Int = 0,
val title: String = "",
val position: Int = 0,
val tasks: List<KanbanTask> = emptyList()
)
@Keep
@Serializable
data class KanbanTask(
val id: Int = 0,
val columnId: Int = 0,
val title: String = "",
val description: String? = null,
val position: Int = 0,
val column: KanbanTaskColumn? = null
)
@Keep
@Serializable
data class KanbanTaskColumn(
val board: KanbanBoard = KanbanBoard()
)
@Keep
data class KanbanTitleRequest(@SerializedName("title") val title: String)
@Keep
data class KanbanTaskRequest(
@SerializedName("title") val title: String,
@SerializedName("description") val description: String? = null
)
@Keep
data class KanbanMoveTaskRequest(@SerializedName("columnId") val columnId: Int)
@Keep
data class SendMessageRequest(
@SerializedName("text") val text: String,
@SerializedName("kanbanBoardId") val kanbanBoardId: Int? = null,
@SerializedName("kanbanTaskId") val kanbanTaskId: Int? = null
)

View File

@@ -0,0 +1,3 @@
package com.aiwazian.messenger.data
data class LocalAccount(val id: Long, val isCurrent: Boolean)

View File

@@ -0,0 +1,8 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
@Keep
data class LoginAvailability(
@Keep val available: Boolean
)

View File

@@ -0,0 +1,58 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import kotlinx.serialization.Serializable
import com.google.gson.annotations.SerializedName
@Keep
@Serializable
data class Message(
@Keep var id: Int = 0,
@Keep val senderId: Long = 0,
@Keep val chatId: Long = 0,
@Keep val text: String? = null,
@Keep val sendTime: Long = 0,
@Keep var isRead: Boolean = false,
@Keep @SerializedName(value = "files", alternate = ["attachments"])
val attachments: Array<Attachment> = emptyArray(),
@Keep val kanbanBoardId: Int? = null,
@Keep val kanbanTaskId: Int? = null,
@Keep val kanbanBoard: KanbanBoard? = null,
@Keep val kanbanTask: KanbanTask? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Message
if (id != other.id) return false
if (senderId != other.senderId) return false
if (chatId != other.chatId) return false
if (sendTime != other.sendTime) return false
if (isRead != other.isRead) return false
if (text != other.text) return false
if (!attachments.contentEquals(other.attachments)) return false
if (kanbanBoardId != other.kanbanBoardId) return false
if (kanbanTaskId != other.kanbanTaskId) return false
if (kanbanBoard != other.kanbanBoard) return false
if (kanbanTask != other.kanbanTask) return false
return true
}
override fun hashCode(): Int {
var result = id
result = 31 * result + senderId.hashCode()
result = 31 * result + chatId.hashCode()
result = 31 * result + sendTime.hashCode()
result = 31 * result + isRead.hashCode()
result = 31 * result + (text?.hashCode() ?: 0)
result = 31 * result + attachments.contentHashCode()
result = 31 * result + (kanbanBoardId ?: 0)
result = 31 * result + (kanbanTaskId ?: 0)
result = 31 * result + (kanbanBoard?.hashCode() ?: 0)
result = 31 * result + (kanbanTask?.hashCode() ?: 0)
return result
}
}

View File

@@ -0,0 +1,8 @@
package com.aiwazian.messenger.data
import androidx.compose.ui.graphics.vector.ImageVector
data class NavigationIcon(
val icon: ImageVector,
val onClick: () -> Unit
)

View File

@@ -0,0 +1,7 @@
package com.aiwazian.messenger.data
data class Notification(
val chatId: Long,
val title: String,
val message: String
)

View File

@@ -0,0 +1,7 @@
package com.aiwazian.messenger.data
data class NotificationChannelInfo(
val id: String,
val name: String,
val description: String
)

View File

@@ -0,0 +1,8 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
@Keep
data class NotificationTokenRequest (
@Keep val token: String
)

View File

@@ -0,0 +1,13 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import com.aiwazian.messenger.enums.PrivacyLevel
@Keep
data class PrivacySettings(
@Keep val bio: Int = PrivacyLevel.Everybody.ordinal,
@Keep val dateOfBirth: Int = PrivacyLevel.Everybody.ordinal,
@Keep val lastSeen: Int = PrivacyLevel.Everybody.ordinal,
@Keep val messages: Int = PrivacyLevel.Everybody.ordinal,
@Keep val invites: Int = PrivacyLevel.Everybody.ordinal
)

View File

@@ -0,0 +1,9 @@
package com.aiwazian.messenger.data
import kotlinx.serialization.Serializable
@Serializable
data class ReadMessagePayload(
val chatId: Long,
val messageId: Int
)

View File

@@ -0,0 +1,11 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
@Keep
data class RegisterRequest (
@Keep val login: String,
@Keep val password: String,
@Keep val firstName: String,
@Keep val lastName: String? = null,
)

View File

@@ -0,0 +1,8 @@
package com.aiwazian.messenger.data
import androidx.compose.runtime.Composable
data class ScreenEntry(
val content: @Composable () -> Unit,
val canGoBackBySwipe: Boolean = true
)

View File

@@ -0,0 +1,10 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
@Keep
data class SearchInfo(
@Keep val chatId: Long,
@Keep val name: String,
@Keep val publicLink: String
)

View File

@@ -0,0 +1,12 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import kotlinx.serialization.Serializable
@Keep
@Serializable
data class SessionInfo(
@Keep val id: Int = 0,
@Keep var deviceName: String = "",
@Keep val createdAt: String = "",
)

View File

@@ -0,0 +1,9 @@
package com.aiwazian.messenger.data
import androidx.compose.ui.graphics.vector.ImageVector
data class TopBarAction(
val icon: ImageVector,
val onClick: (() -> Unit)? = null,
val dropdownActions: List<DropdownMenuAction> = emptyList()
)

View File

@@ -0,0 +1,29 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import com.aiwazian.messenger.interfaces.Profile
@Keep
data class UserInfo(
@Keep override var id: Long = 0,
@Keep var firstName: String = "",
@Keep var lastName: String = "",
@Keep var username: String? = null,
@Keep var bio: String = "",
@Keep var dateOfBirth: Long? = null,
): Profile
@Keep
data class UpdateProfileRequest(
@SerializedName("firstName") val firstName: String,
@SerializedName("lastName") val lastName: String? = null,
@SerializedName("username") val username: String? = null,
@SerializedName("bio") val bio: String? = null,
@SerializedName("dateOfBirth") val dateOfBirth: Long? = null
)
@Keep
data class UpdateUsernameRequest(
@SerializedName("username") val username: String?
)

View File

@@ -0,0 +1,13 @@
package com.aiwazian.messenger.data
import androidx.annotation.Keep
import com.aiwazian.messenger.enums.WebSocketAction
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Keep
@Serializable
data class WebSocketMessage(
@Keep val action: WebSocketAction,
@Keep val data: JsonObject
)

View File

@@ -0,0 +1,40 @@
package com.aiwazian.messenger.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.aiwazian.messenger.database.dao.AccountDao
import com.aiwazian.messenger.database.dao.ChannelDao
import com.aiwazian.messenger.database.dao.ChatDao
import com.aiwazian.messenger.database.dao.FolderChatDao
import com.aiwazian.messenger.database.dao.FolderDao
import com.aiwazian.messenger.database.dao.GroupDao
import com.aiwazian.messenger.database.dao.UserDao
import com.aiwazian.messenger.database.entity.AccountEntity
import com.aiwazian.messenger.database.entity.AttachmentEntity
import com.aiwazian.messenger.database.entity.ChannelEntity
import com.aiwazian.messenger.database.entity.FolderChatEntity
import com.aiwazian.messenger.database.entity.FolderEntity
import com.aiwazian.messenger.database.entity.GroupEntity
import com.aiwazian.messenger.database.entity.MessageEntity
import com.aiwazian.messenger.database.entity.UserEntity
@Database(
entities = [FolderEntity::class, FolderChatEntity::class, UserEntity::class, MessageEntity::class, ChannelEntity::class, AccountEntity::class, GroupEntity::class, AttachmentEntity::class],
version = 7
)
abstract class AppDatabase : RoomDatabase() {
abstract fun folderDao(): FolderDao
abstract fun folderChatDao(): FolderChatDao
abstract fun userDao(): UserDao
abstract fun channelDao(): ChannelDao
abstract fun accountDao(): AccountDao
abstract fun groupDao(): GroupDao
abstract fun chatDao(): ChatDao
}

View File

@@ -0,0 +1,67 @@
package com.aiwazian.messenger.database
import android.content.Context
import androidx.room.Room
import com.aiwazian.messenger.database.dao.AccountDao
import com.aiwazian.messenger.database.dao.ChannelDao
import com.aiwazian.messenger.database.dao.ChatDao
import com.aiwazian.messenger.database.dao.FolderChatDao
import com.aiwazian.messenger.database.dao.FolderDao
import com.aiwazian.messenger.database.dao.GroupDao
import com.aiwazian.messenger.database.dao.UserDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).fallbackToDestructiveMigration(true).build()
}
@Provides
fun provideFolderDao(database: AppDatabase): FolderDao {
return database.folderDao()
}
@Provides
fun provideFolderChatDao(database: AppDatabase): FolderChatDao {
return database.folderChatDao()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
@Provides
fun provideChannelDao(database: AppDatabase): ChannelDao {
return database.channelDao()
}
@Provides
fun provideAccount(database: AppDatabase): AccountDao {
return database.accountDao()
}
@Provides
fun provideGroup(database: AppDatabase): GroupDao {
return database.groupDao()
}
@Provides
fun provideChat(database: AppDatabase): ChatDao {
return database.chatDao()
}
}

View File

@@ -0,0 +1,23 @@
package com.aiwazian.messenger.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.aiwazian.messenger.database.entity.AccountEntity
@Dao
interface AccountDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun add(account: AccountEntity)
@Query("SELECT * FROM account WHERE id = :id")
suspend fun get(id: Int): AccountEntity?
@Query("SELECT * FROM account WHERE isCurrent = 1")
suspend fun getMe(): AccountEntity?
@Delete
suspend fun delete(account: AccountEntity)
}

View File

@@ -0,0 +1,28 @@
package com.aiwazian.messenger.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.aiwazian.messenger.database.entity.ChannelEntity
import com.aiwazian.messenger.types.EntityId
@Dao
interface ChannelDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(channelEntity: ChannelEntity)
@Query("SELECT * FROM channel")
suspend fun getAll(): List<ChannelEntity>
@Query("SELECT * FROM channel WHERE id = :id")
suspend fun get(id: Long): ChannelEntity?
@Update
suspend fun update(channelEntity: ChannelEntity)
@Query("DELETE FROM 'channel' WHERE id = :id")
suspend fun delete(id: Long)
}

View File

@@ -0,0 +1,18 @@
package com.aiwazian.messenger.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.aiwazian.messenger.database.entity.AttachmentEntity
@Dao
interface ChatDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(attachment: AttachmentEntity)
@Query("SELECT * FROM attachment WHERE id = :id")
suspend fun get(id: String): AttachmentEntity
}

View File

@@ -0,0 +1,33 @@
package com.aiwazian.messenger.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.aiwazian.messenger.database.entity.FolderChatEntity
import com.aiwazian.messenger.database.entity.MessageEntity
@Dao
interface FolderChatDao {
@Query("SELECT * FROM folderChat WHERE folderId = :id")
suspend fun getAll(id: Int): List<FolderChatEntity>
@Query("SELECT * FROM folderChat WHERE id = :id")
suspend fun get(id: Long): FolderChatEntity?
@Query("SELECT * FROM message WHERE chatId = :id")
suspend fun getMessages(id: Long): List<MessageEntity>
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
suspend fun insertAll(chatEntities: List<FolderChatEntity>)
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
suspend fun insert(chatEntity: FolderChatEntity)
@Delete
suspend fun delete(folderChatEntity: FolderChatEntity)
@Query("DELETE FROM folderChat WHERE id = :id")
suspend fun deleteById(id: Long)
}

View File

@@ -0,0 +1,23 @@
package com.aiwazian.messenger.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.aiwazian.messenger.database.entity.FolderEntity
@Dao
interface FolderDao {
@Query("SELECT * FROM folder")
suspend fun getAll(): List<FolderEntity>
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
suspend fun insertAll(folderEntities: List<FolderEntity>)
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
suspend fun insert(folderEntities: FolderEntity)
@Delete
suspend fun delete(folderEntity: FolderEntity)
}

View File

@@ -0,0 +1,27 @@
package com.aiwazian.messenger.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.aiwazian.messenger.database.entity.GroupEntity
@Dao
interface GroupDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(groupEntity: GroupEntity)
@Query("SELECT * FROM 'group' WHERE id = :id")
suspend fun get(id: Long): GroupEntity?
@Update
suspend fun update(groupEntity: GroupEntity)
@Delete
suspend fun remove(groupEntity: GroupEntity)
@Query("DELETE FROM 'group' WHERE id = :id")
suspend fun delete(id: Long)
}

View File

@@ -0,0 +1,20 @@
package com.aiwazian.messenger.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.aiwazian.messenger.database.entity.UserEntity
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(userEntity: UserEntity)
@Query("SELECT * FROM user WHERE id = :id")
suspend fun get(id: Long): UserEntity?
@Delete
suspend fun delete(userEntity: UserEntity)
}

View File

@@ -0,0 +1,7 @@
package com.aiwazian.messenger.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity("account")
data class AccountEntity(@PrimaryKey val id: Long, val isCurrent: Boolean)

View File

@@ -0,0 +1,13 @@
package com.aiwazian.messenger.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity("attachment")
data class AttachmentEntity(
@PrimaryKey val id: String,
val messageId: Int,
val name: String,
val url: String,
val size: Long
)

View File

@@ -0,0 +1,17 @@
package com.aiwazian.messenger.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity("channel")
data class ChannelEntity(
@PrimaryKey val id: Long,
var name: String,
var bio: String = "",
val ownerId: Long,
val subscribers: Int,
val removedUser: Int,
val channelType: Int,
val publicLink: String?,
val isSubscribed: Boolean = false
)

View File

@@ -0,0 +1,13 @@
package com.aiwazian.messenger.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity("folderChat")
data class FolderChatEntity(
@PrimaryKey var id: Long,
val folderId: Int = 0,
var chatName: String = "",
var isPinned: Boolean = false,
var lastMessageId: Int? = null
)

View File

@@ -0,0 +1,10 @@
package com.aiwazian.messenger.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "folder")
data class FolderEntity(
@PrimaryKey val id: Int,
val folderName: String = ""
)

View File

@@ -0,0 +1,13 @@
package com.aiwazian.messenger.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity("group")
data class GroupEntity(
@PrimaryKey val id: Long,
val ownerId: Long,
var name: String,
var bio: String,
val members: Int
)

View File

@@ -0,0 +1,14 @@
package com.aiwazian.messenger.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity("message")
data class MessageEntity(
@PrimaryKey var id: Int,
val senderId: Long,
val chatId: Long,
val text: String? = null,
val sendTime: Long = 0,
var isRead: Boolean = false
)

View File

@@ -0,0 +1,14 @@
package com.aiwazian.messenger.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity("user")
data class UserEntity(
@PrimaryKey var id: Long,
var firstName: String = "",
var lastName: String = "",
var username: String? = null,
var bio: String = "",
var dateOfBirth: Long? = null
)

View File

@@ -0,0 +1,18 @@
package com.aiwazian.messenger.database.mappers
import com.aiwazian.messenger.data.LocalAccount
import com.aiwazian.messenger.database.entity.AccountEntity
fun LocalAccount.toEntity(): AccountEntity {
return AccountEntity(
id = this.id,
isCurrent = this.isCurrent
)
}
fun AccountEntity.toLocal(): LocalAccount {
return LocalAccount(
id = this.id,
isCurrent = this.isCurrent
)
}

View File

@@ -0,0 +1,24 @@
package com.aiwazian.messenger.database.mappers
import com.aiwazian.messenger.data.Attachment
import com.aiwazian.messenger.database.entity.AttachmentEntity
fun Attachment.toEntity(): AttachmentEntity {
return AttachmentEntity(
id = this.id,
messageId = this.messageId,
name = this.name,
url = this.url,
size = this.size
)
}
fun AttachmentEntity.toModel(): Attachment {
return Attachment(
id = this.id,
messageId = this.messageId,
name = this.name,
url = this.url,
size = this.size
)
}

View File

@@ -0,0 +1,32 @@
package com.aiwazian.messenger.database.mappers
import com.aiwazian.messenger.data.ChannelInfo
import com.aiwazian.messenger.database.entity.ChannelEntity
fun ChannelInfo.toEntity(): ChannelEntity {
return ChannelEntity(
id = this.id,
name = this.name,
bio = this.bio,
ownerId = this.ownerId,
subscribers = this.subscribers,
removedUser = this.removedUser,
channelType = this.channelType,
publicLink = this.publicLink,
isSubscribed = this.isSubscribed
)
}
fun ChannelEntity.toChannel(): ChannelInfo {
return ChannelInfo(
id = this.id,
name = this.name,
bio = this.bio,
ownerId = this.ownerId,
subscribers = this.subscribers,
removedUser = this.removedUser,
channelType = this.channelType,
publicLink = this.publicLink,
isSubscribed = this.isSubscribed
)
}

View File

@@ -0,0 +1,26 @@
package com.aiwazian.messenger.database.mappers
import com.aiwazian.messenger.data.ChatInfo
import com.aiwazian.messenger.database.entity.FolderChatEntity
import com.aiwazian.messenger.enums.ChatType
fun ChatInfo.toEntity(
folderId: Int,
lastMessageId: Int? = null
): FolderChatEntity {
return FolderChatEntity(
id = this.id,
chatName = this.chatName,
isPinned = this.isPinned,
folderId = folderId,
lastMessageId = lastMessageId
)
}
fun FolderChatEntity.toChat(): ChatInfo {
return ChatInfo(
id = this.id,
chatName = this.chatName,
isPinned = this.isPinned
)
}

View File

@@ -0,0 +1,17 @@
package com.aiwazian.messenger.database.mappers
import com.aiwazian.messenger.data.FolderInfo
import com.aiwazian.messenger.database.entity.FolderEntity
fun FolderInfo.toEntity(): FolderEntity {
return FolderEntity(
id = this.id,
folderName = this.name
)
}
fun FolderEntity.toFolder(): FolderInfo {
return FolderInfo(
id = this.id,
name = this.folderName
)
}

View File

@@ -0,0 +1,24 @@
package com.aiwazian.messenger.database.mappers
import com.aiwazian.messenger.data.GroupInfo
import com.aiwazian.messenger.database.entity.GroupEntity
fun GroupInfo.toEntity(): GroupEntity {
return GroupEntity(
id = this.id,
name = this.name,
bio = this.bio,
ownerId = this.ownerId,
members = this.members
)
}
fun GroupEntity.toGroup(): GroupInfo {
return GroupInfo(
id = this.id,
name = this.name,
bio = this.bio,
ownerId = this.ownerId,
members = this.members
)
}

View File

@@ -0,0 +1,26 @@
package com.aiwazian.messenger.database.mappers
import com.aiwazian.messenger.data.Message
import com.aiwazian.messenger.database.entity.MessageEntity
fun Message.toEntity(): MessageEntity {
return MessageEntity(
id = this.id,
senderId = this.senderId,
chatId = this.chatId,
text = this.text,
sendTime = this.sendTime,
isRead = this.isRead
)
}
fun MessageEntity.toMessage(): Message {
return Message(
id = this.id,
senderId = this.senderId,
chatId = this.chatId,
text = this.text,
sendTime = this.sendTime,
isRead = this.isRead
)
}

View File

@@ -0,0 +1,26 @@
package com.aiwazian.messenger.database.mappers
import com.aiwazian.messenger.data.UserInfo
import com.aiwazian.messenger.database.entity.UserEntity
fun UserInfo.toEntity(): UserEntity {
return UserEntity(
id = this.id,
firstName = this.firstName,
lastName = this.lastName,
username = this.username,
bio = this.bio,
dateOfBirth = this.dateOfBirth
)
}
fun UserEntity.toUser(): UserInfo {
return UserInfo(
id = this.id,
firstName = this.firstName,
lastName = this.lastName,
username = this.username,
bio = this.bio,
dateOfBirth = this.dateOfBirth
)
}

View File

@@ -0,0 +1,12 @@
package com.aiwazian.messenger.database.repository
import com.aiwazian.messenger.data.LocalAccount
import com.aiwazian.messenger.database.dao.AccountDao
import com.aiwazian.messenger.database.mappers.toLocal
import javax.inject.Inject
class AccountRepository @Inject constructor(private val accountDao: AccountDao) {
suspend fun getCurrent(): LocalAccount? {
return accountDao.getMe()?.toLocal()
}
}

View File

@@ -0,0 +1,179 @@
package com.aiwazian.messenger.database.repository
import android.util.Log
import com.aiwazian.messenger.data.ChannelInfo
import com.aiwazian.messenger.data.UserInfo
import com.aiwazian.messenger.database.dao.ChannelDao
import com.aiwazian.messenger.database.mappers.toChannel
import com.aiwazian.messenger.database.mappers.toEntity
import com.aiwazian.messenger.services.ChannelService
import com.aiwazian.messenger.types.EntityId
import javax.inject.Inject
class ChannelRepository @Inject constructor(
private val channelService: ChannelService,
private val channelDao: ChannelDao
) {
suspend fun get(id: Long): ChannelInfo? {
try {
val channel = channelService.get(id)
if (channel != null) {
channelDao.insert(channel.toEntity())
return channel
}
return channelDao.get(id)?.toChannel()
} catch (e: Exception) {
Log.e(
"ChannelRepository",
"Ошибка при получении канала",
e
)
}
val localChannel = channelDao.get(id)
return localChannel?.toChannel()
}
suspend fun create(channelInfo: ChannelInfo): Long? {
try {
val createdId = channelService.create(channelInfo)
if (createdId == null) {
return null
}
channelDao.insert(channelInfo.toEntity())
return createdId
} catch (e: Exception) {
Log.e(
"ChannelRepository",
"Ошибка при создании канала",
e
)
return null
}
}
suspend fun save(channelInfo: ChannelInfo): Long? {
try {
val savedId = channelService.save(channelInfo)
if (savedId == null) {
return null
}
channelDao.insert(channelInfo.toEntity())
return savedId
} catch (e: Exception) {
Log.e(
"ChannelRepository",
"Ошибка при сохранении канала",
e
)
return null
}
}
suspend fun delete(id: Long): Boolean {
try {
val isDeleted = channelService.delete(id)
if (isDeleted) {
channelDao.delete(id)
}
return isDeleted
} catch (e: Exception) {
Log.e(
"ChannelRepository",
"Ошибка при удалении канала",
e
)
return false
}
}
suspend fun getSubscribers(id: Long): List<UserInfo> {
try {
val subscribers = channelService.getSubscribers(id)
return subscribers ?: emptyList()
} catch (e: Exception) {
Log.e(
"ChannelRepository",
"Ошибка при получении подписчиков канала",
e
)
return emptyList()
}
}
suspend fun join(id: Long): Boolean {
try {
channelService.join(id)
val channel = channelDao.get(id)
if (channel != null) {
channelDao.update(channel.copy(isSubscribed = true))
}
return true
} catch (e: Exception) {
Log.e(
"ChannelRepository",
"Ошибка при подписке на канал",
e
)
return false
}
}
suspend fun checkIsBusyPublicLink(link: String): Boolean? {
return try {
channelService.isBusyPublicLick(link)
} catch (e: Exception) {
Log.e(
"ChannelRepository",
"Ошибка при проверке публичной ссылки канала",
e
)
null
}
}
suspend fun leave(id: Long): Boolean {
try {
val channel = channelDao.get(id)
if (channel != null) {
channelDao.update(channel.copy(isSubscribed = false))
}
channelService.leave(id)
return true
} catch (e: Exception) {
Log.e(
"ChannelRepository",
"Ошибка при отписке от канала",
e
)
return false
}
}
}

View File

@@ -0,0 +1,166 @@
package com.aiwazian.messenger.database.repository
import android.util.Log
import com.aiwazian.messenger.data.Attachment
import com.aiwazian.messenger.data.ChatInfo
import com.aiwazian.messenger.data.Message
import com.aiwazian.messenger.database.dao.ChatDao
import com.aiwazian.messenger.database.dao.FolderChatDao
import com.aiwazian.messenger.database.mappers.toChat
import com.aiwazian.messenger.database.mappers.toEntity
import com.aiwazian.messenger.database.mappers.toMessage
import com.aiwazian.messenger.database.mappers.toModel
import com.aiwazian.messenger.services.ChatService
import javax.inject.Inject
class ChatRepository @Inject constructor(
private val chatService: ChatService,
private val folderChatDao: FolderChatDao,
private val chatDao: ChatDao
) {
suspend fun get(id: Long): ChatInfo? {
try {
val chat = chatService.getChatInfo(id)
if (chat != null) {
return chat
}
} catch (e: Exception) {
Log.e(
"ChatRepository",
"Ошибка при получении информации о чате",
e
)
}
val localChat = folderChatDao.get(id)
return localChat?.toChat()
}
suspend fun getMessages(id: Long): List<Message> {
try {
val messages = chatService.getChatMessages(id)
if (messages != null) {
return messages
}
} catch (e: Exception) {
Log.e(
"ChatRepository",
"Ошибка при получении сообщений",
e
)
}
val localMessages = folderChatDao.getMessages(id)
if (localMessages.isEmpty()) {
return emptyList()
}
return localMessages.map { it.toMessage() }
}
suspend fun getLastMessage(id: Long): Message? {
return chatService.getChatLastMessage(id)
}
suspend fun sendMessage(chatId: Long, message: Message): Message? {
return chatService.sendMessage(chatId, message)
}
suspend fun saveAttachment(attachment: Attachment) {
chatDao.save(attachment.toEntity())
}
suspend fun getAttachment(id: String): Attachment {
return chatDao.get(id).toModel()
}
suspend fun makeAsRead(
chatId: Long,
messageId: Int
): Boolean {
return chatService.makeAsReadMessage(
chatId,
messageId
)
}
suspend fun deleteMessage(
chatId: Long,
messageId: Int,
deleteForAll: Boolean
): Boolean {
try {
return chatService.deleteMessage(
chatId,
messageId,
deleteForAll
)
} catch (e: Exception) {
Log.e(
"ChatRepository",
"Ошибка при удалени сообщения",
e
)
return false
}
}
suspend fun deleteChat(
chatId: Long
) {
try {
folderChatDao.deleteById(chatId)
} catch (e: Exception) {
Log.e(
"ChatRepository",
"Ошибка при удалении чата",
e
)
}
}
suspend fun deleteChatMessages(
chatId: Long,
deleteForReceiver: Boolean
): Boolean {
return chatService.deleteChatMessages(
chatId,
deleteForReceiver
)
}
suspend fun pin(
chatId: Long,
folderId: Int
): Boolean {
return chatService.pin(
chatId,
folderId
)
}
suspend fun unpin(
chatId: Long,
folderId: Int
): Boolean {
return chatService.unpin(
chatId,
folderId
)
}
suspend fun archive(id: Long): Boolean {
return chatService.archiveChat(id)
}
suspend fun unarchive(id: Long): Boolean {
return chatService.unarchiveChat(id)
}
}

View File

@@ -0,0 +1,110 @@
package com.aiwazian.messenger.database.repository
import android.util.Log
import com.aiwazian.messenger.api.RetrofitInstance
import com.aiwazian.messenger.data.ChatInfo
import com.aiwazian.messenger.data.FolderInfo
import com.aiwazian.messenger.database.dao.FolderChatDao
import com.aiwazian.messenger.database.dao.FolderDao
import com.aiwazian.messenger.database.mappers.toEntity
import com.aiwazian.messenger.database.mappers.toChat
import com.aiwazian.messenger.database.mappers.toFolder
import com.aiwazian.messenger.services.FolderService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FolderRepository @Inject constructor(
private val folderService: FolderService,
private val folderDao: FolderDao,
private val folderChatDao: FolderChatDao
) {
private val _folders = MutableStateFlow<List<FolderInfo>>(emptyList())
val folders = _folders.asStateFlow()
suspend fun loadFolders() {
val localFolderEntities = folderDao.getAll()
val localFolders = localFolderEntities.map { it.toFolder() }
_folders.update {
localFolders.map { folder ->
folder.chats = folderChatDao.getAll(folder.id).map { it.toChat() }
folder
}
}
try {
val folders = folderService.getAll().orEmpty()
val chatsResponse = RetrofitInstance.api.getUnarchivedChats()
if (!chatsResponse.isSuccessful) return
val response = chatsResponse.body().orEmpty()
val chatFolderInfos = listOf(
FolderInfo(
id = 0,
name = "Все чаты",
chats = response
)
) + folders
_folders.update { chatFolderInfos }
val folderEntities = _folders.value.map { it.toEntity() }
folderDao.insertAll(folderEntities)
_folders.value.forEach { folder ->
val chatEntities = folder.chats.map { it.toEntity(folder.id) }
folderChatDao.insertAll(chatEntities)
}
} catch (e: Exception) {
Log.e(
"FolderRepository",
"Ошибка при получении папок с чатами",
e
)
}
}
fun getFolderChats(folderId: Int): List<ChatInfo> {
return _folders.value.find { it.id == folderId }?.chats ?: emptyList()
}
suspend fun saveFolder(folderInfo: FolderInfo) {
if (folderInfo.id == 0) {
folderInfo.id = (_folders.value.maxOfOrNull { it.id } ?: 0) + 1
}
_folders.update { currentFolders ->
val existingIndex = currentFolders.indexOfFirst { it.id == folderInfo.id }
if (existingIndex != -1) {
currentFolders.toMutableList().apply {
this[existingIndex] = folderInfo
}
} else {
currentFolders + folderInfo
}
}
folderDao.insertAll(listOf(folderInfo.toEntity()))
folderChatDao.insertAll(folderInfo.chats.map { it.toEntity(folderInfo.id) })
}
suspend fun remove(folderId: Int): Boolean {
val folder = _folders.value.find { it.id == folderId }?.toEntity()
if (folder == null) {
return false
}
_folders.update { it.filter { it -> it.id != folderId } }
folderDao.delete(folder)
return true
}
}

View File

@@ -0,0 +1,78 @@
package com.aiwazian.messenger.database.repository
import android.util.Log
import com.aiwazian.messenger.data.GroupInfo
import com.aiwazian.messenger.database.dao.GroupDao
import com.aiwazian.messenger.database.mappers.toEntity
import com.aiwazian.messenger.database.mappers.toGroup
import com.aiwazian.messenger.services.GroupService
import javax.inject.Inject
class GroupRepository @Inject constructor(
private val groupService: GroupService,
private val groupDao: GroupDao
) {
suspend fun create(groupInfo: GroupInfo): Long? {
try {
val createdId = groupService.create(groupInfo)
if (createdId == null) {
return null
}
groupDao.insert(groupInfo.toEntity())
return createdId
} catch (e: Exception) {
Log.e(
"GroupRepository",
"Ошибка при создании канала",
e
)
return null
}
}
suspend fun get(id: Long): GroupInfo? {
try {
val group = groupService.get(id)
if (group != null) {
groupDao.insert(group.toEntity())
return group
}
return groupDao.get(id)?.toGroup()
} catch (e: Exception) {
Log.e(
"GroupRepository",
"Ошибка при получении группы",
e
)
return null
}
}
suspend fun delete(id: Long): Boolean {
try {
val isDeleted = groupService.delete(id)
if (isDeleted) {
groupDao.delete(id)
}
return isDeleted
} catch (e: Exception) {
Log.e(
"GroupRepository",
"Ошибка при получении участников группы",
e
)
return false
}
}
}

View File

@@ -0,0 +1,88 @@
package com.aiwazian.messenger.database.repository
import android.util.Log
import com.aiwazian.messenger.api.RetrofitInstance
import com.aiwazian.messenger.data.UserInfo
import com.aiwazian.messenger.database.dao.AccountDao
import com.aiwazian.messenger.database.dao.UserDao
import com.aiwazian.messenger.database.entity.AccountEntity
import com.aiwazian.messenger.database.mappers.toEntity
import com.aiwazian.messenger.database.mappers.toUser
import com.aiwazian.messenger.services.UserService
import javax.inject.Inject
class UserRepository @Inject constructor(
private val userService: UserService,
private val userDao: UserDao,
private val accountDao: AccountDao
) {
suspend fun getMe(): UserInfo? {
try {
val response = RetrofitInstance.api.getMe()
val user = response.body()
if (user != null) {
val userEntity = user.toEntity()
userDao.insert(userEntity)
val accountEntity = AccountEntity(id = userEntity.id, isCurrent = true)
accountDao.add(accountEntity)
return user
}
} catch (e: Exception) {
Log.e(
"UserRepository",
"Ошибка при запросе Get Me",
e
)
}
val accountEntity = accountDao.getMe()
if (accountEntity == null) {
return null
}
val user = userDao.get(accountEntity.id)
return user?.toUser()
}
suspend fun getById(id: Long): UserInfo? {
try {
val user = userService.getById(id)
if (user != null) {
userDao.insert(user.toEntity())
return user
}
} catch (e: Exception) {
Log.e(
"UserRepository",
"Ошибка при получении профиля",
e
)
}
val localUser = userDao.get(id)
return localUser?.toUser()
}
suspend fun updateProfile(user: UserInfo): Boolean {
try {
userDao.insert(user.toEntity())
return userService.updateProfile(user)
} catch (e: Exception) {
Log.e(
"UserRepository",
"Ошибка при обновлении профиля",
e
)
return false
}
}
}

View File

@@ -0,0 +1,12 @@
package com.aiwazian.messenger.enums
enum class ChannelType {
PUBLIC,
PRIVATE;
companion object {
fun fromInt(value: Int): ChannelType {
return entries.first { it.ordinal == value }
}
}
}

View File

@@ -0,0 +1,26 @@
package com.aiwazian.messenger.enums
enum class ChatType {
PRIVATE,
GROUP,
CHANNEL,
UNKNOWN;
companion object {
fun fromOrdinal(ordinal: Int): ChatType {
return entries.firstOrNull { it.ordinal == ordinal } ?: UNKNOWN
}
fun fromId(id: Long): ChatType {
val idString = id.toString()
val firstDigit = idString[0].digitToInt()
return when (firstDigit) {
1-> PRIVATE
2-> CHANNEL
3-> GROUP
else -> UNKNOWN
}
}
}
}

View File

@@ -0,0 +1,7 @@
package com.aiwazian.messenger.enums
enum class DownloadStatus {
PENDING,
DOWNLOADING,
COMPLETED
}

View File

@@ -0,0 +1,48 @@
package com.aiwazian.messenger.enums
enum class FileType {
IMAGE,
VIDEO,
MUSIC,
ZIP,
TEXT,
HTML,
CSS,
JAVASCRIPT,
PHP,
APK,
GIF,
JSON,
OTHER;
companion object {
private val extensionMap: Map<String, FileType> = mapOf(
"jpg" to IMAGE,
"jpeg" to IMAGE,
"png" to IMAGE,
"gif" to IMAGE,
"bmp" to IMAGE,
"mp4" to VIDEO,
"avi" to VIDEO,
"mkv" to VIDEO,
"mov" to VIDEO,
"mp3" to MUSIC,
"wav" to MUSIC,
"aac" to MUSIC,
"flac" to MUSIC,
"zip" to ZIP,
"txt" to TEXT,
"html" to HTML,
"css" to CSS,
"js" to JAVASCRIPT,
"php" to PHP,
"gif" to GIF,
"apk" to APK,
"json" to JSON
)
fun fromExtension(extension: String): FileType {
return extensionMap[extension.lowercase()] ?: OTHER
}
}
}

View File

@@ -0,0 +1,20 @@
package com.aiwazian.messenger.enums
import androidx.compose.ui.graphics.Color
enum class PrimaryColorOption(val color: Color) {
Blue(Color(0xFF2196F3)),
Green(Color(0xFF4CAF50)),
DarkGreen(Color(0xFF009688)),
Purple(Color(0xFF9C27B0)),
Orange(Color(0xFFFF5722)),
Orange1(Color(0xFFE91E63)),
Pink(Color(0xFFFF00FF)),
Pink1(Color(0xFF673AB7));
companion object {
fun fromString(value: String): PrimaryColorOption {
return entries.firstOrNull { it.name.equals(value, ignoreCase = true) } ?: Blue
}
}
}

View File

@@ -0,0 +1,12 @@
package com.aiwazian.messenger.enums
enum class PrivacyLevel(val id: Int) {
Everybody(0),
Nobody(1);
companion object {
fun fromId(id: Int): PrivacyLevel {
return entries.first { it.id == id }
}
}
}

View File

@@ -0,0 +1,18 @@
package com.aiwazian.messenger.enums
enum class ThemeOption {
LIGHT,
DARK,
SYSTEM;
companion object {
fun fromString(value: String): ThemeOption {
return entries.firstOrNull {
it.name.equals(
value,
ignoreCase = true
)
} ?: SYSTEM
}
}
}

View File

@@ -0,0 +1,29 @@
package com.aiwazian.messenger.enums
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
enum class WebSocketAction {
@SerialName("NEW_MESSAGE")
NEW_MESSAGE,
@SerialName("DELETE_MESSAGE")
DELETE_MESSAGE,
@SerialName("DELETE_CHAT")
DELETE_CHAT,
@SerialName("READ_MESSAGE")
READ_MESSAGE,
@SerialName("NEW_CHAT")
NEW_CHAT,
@SerialName("HISTORY_CLEAR")
HISTORY_CLEAR,
KANBAN_UPDATE,
UNKNOWN
}

View File

@@ -0,0 +1,311 @@
package com.aiwazian.messenger.interfaces
import androidx.annotation.Keep
import com.aiwazian.messenger.data.ApiResponse
import com.aiwazian.messenger.data.AuthRequest
import com.aiwazian.messenger.data.FileDownloadUrlResponse
import com.aiwazian.messenger.data.AuthResponse
import com.aiwazian.messenger.data.ChangeCloudPasswordRequest
import com.aiwazian.messenger.data.ChannelInfo
import com.aiwazian.messenger.data.CreateChannelRequest
import com.aiwazian.messenger.data.CreateGroupRequest
import com.aiwazian.messenger.data.CreatedEntityResponse
import com.aiwazian.messenger.data.ChatInfo
import com.aiwazian.messenger.data.FolderInfo
import com.aiwazian.messenger.data.FileUploadConfirmRequest
import com.aiwazian.messenger.data.FileUploadInitRequest
import com.aiwazian.messenger.data.FileUploadInitResponse
import com.aiwazian.messenger.data.GroupInfo
import com.aiwazian.messenger.data.LoginAvailability
import com.aiwazian.messenger.data.KanbanBoard
import com.aiwazian.messenger.data.KanbanMoveTaskRequest
import com.aiwazian.messenger.data.KanbanTaskRequest
import com.aiwazian.messenger.data.KanbanTitleRequest
import com.aiwazian.messenger.data.Message
import com.aiwazian.messenger.data.NotificationTokenRequest
import com.aiwazian.messenger.data.PrivacySettings
import com.aiwazian.messenger.data.RegisterRequest
import com.aiwazian.messenger.data.SearchInfo
import com.aiwazian.messenger.data.SessionInfo
import com.aiwazian.messenger.data.SendMessageRequest
import com.aiwazian.messenger.data.UserInfo
import com.aiwazian.messenger.data.UsernameAvailability
import com.aiwazian.messenger.data.UpdateProfileRequest
import com.aiwazian.messenger.data.UpdateUsernameRequest
import com.aiwazian.messenger.utils.Route
import okhttp3.MultipartBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Streaming
import retrofit2.http.Url
@Keep
interface ApiService {
@GET("api/kanban")
suspend fun getKanbanBoards(): Response<List<KanbanBoard>>
@POST("api/kanban")
suspend fun createKanbanBoard(@Body request: KanbanTitleRequest): Response<KanbanBoard>
@PATCH("api/kanban/{boardId}")
suspend fun renameKanbanBoard(
@Path("boardId") boardId: Int,
@Body request: KanbanTitleRequest
): Response<KanbanBoard>
@DELETE("api/kanban/{boardId}")
suspend fun deleteKanbanBoard(@Path("boardId") boardId: Int): Response<Unit>
@POST("api/kanban/{boardId}/columns")
suspend fun createKanbanColumn(@Path("boardId") boardId: Int, @Body request: KanbanTitleRequest): Response<KanbanBoard>
@POST("api/kanban/columns/{columnId}/tasks")
suspend fun createKanbanTask(@Path("columnId") columnId: Int, @Body request: KanbanTaskRequest): Response<KanbanBoard>
@PATCH("api/kanban/tasks/{taskId}")
suspend fun moveKanbanTask(@Path("taskId") taskId: Int, @Body request: KanbanMoveTaskRequest): Response<KanbanBoard>
@DELETE("api/kanban/tasks/{taskId}")
suspend fun deleteKanbanTask(@Path("taskId") taskId: Int): Response<KanbanBoard>
@GET(Route.FIND_USER_BY_LOGIN)
suspend fun findUserByLogin(@Path("login") login: String): Response<LoginAvailability>
@POST(Route.LOGIN)
suspend fun login(@Body request: AuthRequest): Response<AuthResponse>
@POST(Route.REGISTER)
suspend fun register(@Body request: RegisterRequest): Response<Unit>
@POST(Route.LOGOUT)
suspend fun logout(): Response<ApiResponse>
@GET(Route.ME)
suspend fun getMe(): Response<UserInfo>
@GET(Route.UNARCHIVED_CHATS)
suspend fun getUnarchivedChats(): Response<List<ChatInfo>>
@GET(Route.ARCHIVED_CHATS)
suspend fun getArchivedChats(): Response<List<ChatInfo>>
@GET(Route.GET_SESSIONS)
suspend fun getSessions(): Response<List<SessionInfo>>
@POST(Route.UPDATE_FCM_TOKEN)
suspend fun updateFcmToken(@Body newToken: NotificationTokenRequest): Response<ApiResponse>
@DELETE(Route.TERMINATE_ALL_SESSIONS)
suspend fun terminateAllSessions(): Response<ApiResponse>
@DELETE(Route.TERMINATE_SESSION)
suspend fun terminateSession(@Path("id") id: Int): Response<ApiResponse>
@GET(Route.GET_DEVICE_COUNT)
suspend fun getDeviceCount(): Response<Int>
@DELETE(Route.DELETE_CHAT)
suspend fun deleteChat(
@Path("id") chatId: Long,
@Query("deleteForReceiver") deleteForReceiver: Boolean
): Response<Unit>
@DELETE(Route.DELETE_CHAT_MESSAGES)
suspend fun deleteChatMessages(
@Path("id") id: Long,
@Query("deleteForReceiver") deleteForReceiver: Boolean
): Response<Unit>
@PATCH(Route.CHANGE_CLOUD_PASSWORD)
suspend fun changeCloudPassword(@Body body: ChangeCloudPasswordRequest): Response<ApiResponse>
@PATCH(Route.CHANGE_BIO_PRIVACY)
suspend fun changeBioPrivacy(@Path("value") body: Int): Response<ApiResponse>
@PATCH(Route.CHANGE_DATE_OF_BIRTH_PRIVACY)
suspend fun changeDateOfBirthPrivacy(@Path("value") body: Int): Response<ApiResponse>
@GET(Route.CHAT_MESSAGES)
suspend fun getMessagesBetweenUsers(@Path("id") chatId: Long): Response<List<Message>>
@GET(Route.GET_CHAT_LAST_MESSAGE)
suspend fun getChatLastMessage(@Path("chatId") chatId: Long): Response<Message>
@GET(Route.GET_CHAT_INFO)
suspend fun getChatInfo(@Path("id") id: Long): Response<ChatInfo?>
@PATCH("api/users/me")
suspend fun updateProfile(@Body profile: UpdateProfileRequest): Response<UserInfo>
@GET("api/search")
suspend fun searchUser(@Query("q") query: String): Response<List<SearchInfo>>
@GET(Route.GE_USER_BY_ID)
suspend fun getUserById(@Path("id") id: Long): Response<UserInfo>
@POST(Route.ADD_CHAT_TO_ARCHIVE)
suspend fun archiveChat(@Path("id") chatId: Long): Response<ApiResponse>
@DELETE(Route.DELETE_CHAT_FROM_ARCHIVE)
suspend fun unarchiveChat(@Path("id") chatId: Long): Response<ApiResponse>
@POST(Route.SEND_MESSAGE)
suspend fun sendMessage(
@Path("chatId") chatId: Long,
@Body requestBody: SendMessageRequest
): Response<Message>
@Multipart
@POST(Route.SEND_DOCUMENT)
suspend fun sendDocument(
@Part file: MultipartBody.Part,
@Path("chatId") chatId: Long
): Response<Message>
@POST("api/chats/{chatId}/messages/files/init")
suspend fun initFileUpload(
@Path("chatId") chatId: Long,
@Body request: FileUploadInitRequest
): Response<FileUploadInitResponse>
@POST("api/chats/{chatId}/messages/files/confirm")
suspend fun confirmFileUpload(
@Path("chatId") chatId: Long,
@Body request: FileUploadConfirmRequest
): Response<Message>
@GET("api/chats/{chatId}/messages/{messageId}/files/{fileId}/download")
suspend fun getFileDownloadUrl(
@Path("chatId") chatId: Long,
@Path("messageId") messageId: Int,
@Path("fileId") fileId: String
): Response<FileDownloadUrlResponse>
@DELETE(Route.DELETE_MESSAGE)
suspend fun deleteMessage(
@Path("chatId") chatId: Long,
@Path("messageId") messageId: Int,
@Query("forEveryone") forEveryone: Boolean
): Response<Unit>
@POST(Route.MAKE_AS_READ_MESSAGE)
suspend fun makeAsReadMessage(
@Path("chatId") chatId: Long,
@Path("messageId") messageId: Int
): Response<Unit>
@POST(Route.FOLDER)
suspend fun saveFolder(@Body requestBody: FolderInfo): Response<ApiResponse>
@DELETE(Route.DELETE_FOLDER)
suspend fun deleteFolder(@Path("id") id: Int): Response<ApiResponse>
@GET(Route.FOLDERS)
suspend fun getFolders(): Response<List<FolderInfo>>
@GET(Route.CHATS)
suspend fun getAllChats(): Response<List<ChatInfo>>
@GET(Route.CHATS)
suspend fun getAllChatsWithOtherUser(): Response<List<ChatInfo>>
@POST(Route.PIN_CHAT)
suspend fun pinChat(
@Path("id") chatId: Long
): Response<ApiResponse>
@DELETE(Route.UNPIN_CHAT)
suspend fun unpinChat(
@Path("id") chatId: Long
): Response<ApiResponse>
@POST(Route.PIN_CHAT_IN_FOLDER)
suspend fun pinChatInFolder(
@Path("folderId") folderId: Int,
@Path("chatId") chatId: Long
): Response<ApiResponse>
@DELETE(Route.UNPIN_CHAT_IN_FOLDER)
suspend fun unpinChatInFolder(
@Path("folderId") folderId: Int,
@Path("chatId") chatId: Long
): Response<ApiResponse>
@GET(Route.GET_MY_PRIVACY)
suspend fun getMyPrivacy(): Response<PrivacySettings>
@PATCH("api/users/me/privacy")
suspend fun updatePrivacy(@Body settings: PrivacySettings): Response<PrivacySettings>
@GET("api/search/check/{username}")
suspend fun checkUsername(@Path("username") username: String): Response<UsernameAvailability>
@PATCH("api/users/me/username")
suspend fun saveUsername(@Body request: UpdateUsernameRequest): Response<UserInfo>
@POST(Route.CREATE_CHANNEL)
suspend fun createChannel(@Body channelInfo: CreateChannelRequest): Response<CreatedEntityResponse>
@POST(Route.SAVE_CHANNEL)
suspend fun saveChannel(
@Path("id") id: Long,
@Body channelInfo: ChannelInfo
): Response<ApiResponse>
@DELETE(Route.DELETE_CHANNEL)
suspend fun deleteChannel(@Path("id") id: Long): Response<ApiResponse>
@GET(Route.GET_CHANNEL)
suspend fun getChannel(@Path("id") id: Long): Response<ChannelInfo>
@POST(Route.JOIN_CHANNEL)
suspend fun joinChannel(@Path("id") id: Long): Response<ApiResponse>
@DELETE(Route.LEAVE_CHANNEL)
suspend fun leaveChannel(@Path("id") id: Long): Response<ApiResponse>
@GET(Route.GET_CHANNEL_SUBSCRIBERS)
suspend fun getChannelSubscribers(@Path("id") id: Long): Response<List<UserInfo>>
@GET(Route.CHECK_CHANNEL_PUBLIC_LINK)
suspend fun checkChannelPublicLink(@Path("link") link: String): Response<ApiResponse>
@POST(Route.CREATE_GROUP)
suspend fun createGroup(@Body groupInfo: CreateGroupRequest): Response<CreatedEntityResponse>
@GET(Route.GET_GROUP)
suspend fun getGroup(@Path("id") id: Long): Response<GroupInfo>
@DELETE(Route.DELETE_GROUP)
suspend fun deleteGroup(@Path("id") id: Long): Response<ApiResponse>
@GET(Route.GET_GROUP_MEMBERS)
suspend fun getGroupMembers(@Path("id") id: Long): Response<List<UserInfo>>
@POST(Route.INVITE_USER_TO_GROUP)
suspend fun inviteUserToGroup(
@Path("groupId") groupId: Long,
@Path("userId") userId: Long
): Response<Unit>
@DELETE(Route.REMOVE_USER_FROM_GROUP)
suspend fun removeUserFromGroup(
@Path("groupId") groupId: Long,
@Path("userId") userId: Long
): Response<Unit>
@Streaming
@GET
fun downloadFile(@Url fileUrl: String): Call<ResponseBody>
}

View File

@@ -0,0 +1,10 @@
package com.aiwazian.messenger.interfaces
import com.aiwazian.messenger.data.Notification
interface NotificationService {
fun showNotification(
notification: Notification,
messages: List<String>
)
}

View File

@@ -0,0 +1,8 @@
package com.aiwazian.messenger.interfaces
import com.google.errorprone.annotations.Keep
@Keep
interface Profile {
val id: Long
}

View File

@@ -0,0 +1,5 @@
package com.aiwazian.messenger.interfaces
interface QrCodeService {
//fun createQrCode()
}

View File

@@ -0,0 +1,64 @@
package com.aiwazian.messenger.services
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AppLockService @Inject constructor() {
private val _isLockApp = MutableStateFlow(false)
val isLockApp = _isLockApp.asStateFlow()
private val _passcode = MutableStateFlow("")
private val _hasPasscode = MutableStateFlow(false)
val hasPasscode = _hasPasscode.asStateFlow()
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val dataStoreManager = DataStoreManager.getInstance()
init {
coroutineScope.launch {
val passcode = dataStoreManager.getPasscode().first()
_passcode.update { passcode }
_hasPasscode.update { _passcode.value.isNotBlank() }
val isLock = dataStoreManager.getIsLockApp().first()
_isLockApp.update { isLock }
}
}
suspend fun lock() {
_isLockApp.update { true }
dataStoreManager.saveIsLockApp(true)
}
suspend fun unlock() {
_isLockApp.update { false }
dataStoreManager.saveIsLockApp(false)
}
suspend fun disablePasscode() {
_hasPasscode.update { false }
dataStoreManager.savePasscode("")
}
suspend fun changePasscode(newPasscode: String) {
_passcode.update { newPasscode }
_hasPasscode.update { true }
dataStoreManager.savePasscode(newPasscode)
}
fun checkPasscode(passcode: String): Boolean {
return passcode == _passcode.value
}
}

View File

@@ -0,0 +1,34 @@
package com.aiwazian.messenger.services
import com.aiwazian.messenger.api.RetrofitInstance
import com.aiwazian.messenger.data.AuthRequest
import com.aiwazian.messenger.data.RegisterRequest
import com.aiwazian.messenger.utils.WebSocketManager
import javax.inject.Inject
class AuthService @Inject constructor() {
suspend fun logout() {
RetrofitInstance.api.logout()
WebSocketManager.close()
TokenManager.setAuthorized(false)
TokenManager.removeToken()
}
suspend fun login(authRequest: AuthRequest): String? {
val response = RetrofitInstance.api.login(authRequest)
return response.body()?.token
}
suspend fun register(registerRequest: RegisterRequest): Boolean {
val response = RetrofitInstance.api.register(registerRequest)
return response.isSuccessful
}
suspend fun findUserByLogin(login: String): Boolean {
val response = RetrofitInstance.api.findUserByLogin(login)
return response.body()?.available == false
}
}

View File

@@ -0,0 +1,60 @@
package com.aiwazian.messenger.services
import com.aiwazian.messenger.api.RetrofitInstance
import com.aiwazian.messenger.data.ChannelInfo
import com.aiwazian.messenger.data.CreateChannelRequest
import com.aiwazian.messenger.enums.ChannelType
import com.aiwazian.messenger.data.UserInfo
import com.aiwazian.messenger.types.EntityId
import javax.inject.Inject
class ChannelService @Inject constructor() {
suspend fun create(channel: ChannelInfo): Long? {
val response = RetrofitInstance.api.createChannel(
CreateChannelRequest(
name = channel.name,
bio = channel.bio.ifBlank { null },
channelType = if (channel.channelType == ChannelType.PUBLIC.ordinal) "PUBLIC" else "PRIVATE",
username = channel.publicLink?.trim()?.trimStart('@')?.ifBlank { null }
)
)
return response.body()?.id
}
suspend fun save(channel: ChannelInfo): Long? {
val response = RetrofitInstance.api.saveChannel(channel.id, channel)
return response.body()?.message?.toLongOrNull()
}
suspend fun delete(id: Long): Boolean {
val response = RetrofitInstance.api.deleteChannel(id)
return response.isSuccessful
}
suspend fun get(id: Long): ChannelInfo? {
val response = RetrofitInstance.api.getChannel(id)
return response.body()
}
suspend fun join(id: Long): Boolean {
val response = RetrofitInstance.api.joinChannel(id)
return response.isSuccessful
}
suspend fun leave(id: Long): Boolean {
val response = RetrofitInstance.api.leaveChannel(id)
return response.isSuccessful
}
suspend fun isBusyPublicLick(link:String): Boolean {
val response = RetrofitInstance.api.checkChannelPublicLink(link)
return !response.isSuccessful
}
suspend fun getSubscribers(id:Long): List<UserInfo>? {
val response = RetrofitInstance.api.getChannelSubscribers(id)
return response.body()
}
}

View File

@@ -0,0 +1,135 @@
package com.aiwazian.messenger.services
import com.aiwazian.messenger.api.RetrofitInstance
import com.aiwazian.messenger.data.ChatInfo
import com.aiwazian.messenger.data.Message
import com.aiwazian.messenger.data.SendMessageRequest
import okhttp3.MultipartBody
import javax.inject.Inject
class ChatService @Inject constructor() {
suspend fun sendMessage(chatId: Long, message: Message): Message? {
val response = RetrofitInstance.api.sendMessage(
chatId,
SendMessageRequest(message.text.orEmpty(), message.kanbanBoardId, message.kanbanTaskId)
)
return if (response.isSuccessful) response.body() else null
}
suspend fun sendDocument(fileUri: MultipartBody.Part, chatId: Long): Message? {
val response = RetrofitInstance.api.sendDocument(fileUri, chatId)
return if (response.isSuccessful) response.body() else null
}
suspend fun getChatInfo(chatId: Long): ChatInfo? {
val response = RetrofitInstance.api.getChatInfo(chatId)
return response.body()
}
suspend fun getAllChatsWithOtherUser(): List<ChatInfo>? {
val response = RetrofitInstance.api.getAllChatsWithOtherUser()
return response.body()
}
suspend fun makeAsReadMessage(
chatId: Long,
messageId: Int
): Boolean {
val response = RetrofitInstance.api.makeAsReadMessage(
chatId,
messageId
)
return response.isSuccessful
}
suspend fun getChatLastMessage(chatId: Long): Message? {
return getChatMessages(chatId)?.lastOrNull()
}
suspend fun getChatMessages(chatId: Long): List<Message>? {
val response = RetrofitInstance.api.getMessagesBetweenUsers(chatId)
return response.body()
}
suspend fun archiveChat(chatId: Long): Boolean {
val response = RetrofitInstance.api.archiveChat(chatId)
return response.isSuccessful
}
suspend fun unarchiveChat(chatId: Long): Boolean {
val response = RetrofitInstance.api.unarchiveChat(chatId)
return response.isSuccessful
}
suspend fun pin(
chatId: Long,
folderId: Int
): Boolean {
val response = if (folderId == 0) {
RetrofitInstance.api.pinChat(chatId)
} else {
RetrofitInstance.api.pinChatInFolder(
folderId,
chatId
)
}
return response.isSuccessful
}
suspend fun unpin(
chatId: Long,
folderId: Int
): Boolean {
val response = if (folderId == 0) {
RetrofitInstance.api.unpinChat(chatId)
} else {
RetrofitInstance.api.unpinChatInFolder(
folderId,
chatId
)
}
return response.isSuccessful
}
suspend fun deleteMessage(
chatId: Long,
messageId: Int,
deleteForAll: Boolean
): Boolean {
val response = RetrofitInstance.api.deleteMessage(
chatId,
messageId,
deleteForAll
)
return response.isSuccessful
}
suspend fun deleteChat(
chatId: Long,
deleteForReceiver: Boolean
): Boolean {
val response = RetrofitInstance.api.deleteChat(
chatId,
deleteForReceiver
)
return response.isSuccessful
}
suspend fun deleteChatMessages(
chatId: Long,
deleteForReceiver: Boolean
): Boolean {
val response = RetrofitInstance.api.deleteChatMessages(
chatId,
deleteForReceiver
)
return response.isSuccessful
}
}

View File

@@ -0,0 +1,16 @@
package com.aiwazian.messenger.services
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
class ClipboardHelper(private val context: Context) {
fun copy(text: String) {
val clipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("label", text)
clipboardManager.setPrimaryClip(clipData)
}
}

View File

@@ -0,0 +1,126 @@
package com.aiwazian.messenger.services
import android.annotation.SuppressLint
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.aiwazian.messenger.enums.PrimaryColorOption
import com.aiwazian.messenger.enums.ThemeOption
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private const val USER_PREFERENCES_NAME = "data_store"
private val Context.dataStore by preferencesDataStore(USER_PREFERENCES_NAME)
private object Keys {
val THEME = stringPreferencesKey("app_theme")
val TOKEN = stringPreferencesKey("token")
val PRIMARY_COLOR = stringPreferencesKey("primary_color")
val PASSCODE = stringPreferencesKey("passcode")
val IS_LOCK_APP = booleanPreferencesKey("is_lock_app")
val DYNAMIC_COLOR = booleanPreferencesKey("dynamic_color")
}
class DataStoreManager private constructor(private val context: Context) {
companion object {
@SuppressLint("StaticFieldLeak")
@Volatile
private var INSTANCE: DataStoreManager? = null
fun initialize(context: Context) {
if (INSTANCE == null) {
synchronized(this) {
if (INSTANCE == null) {
INSTANCE = DataStoreManager(context.applicationContext)
}
}
}
}
fun getInstance(): DataStoreManager {
return INSTANCE ?: throw IllegalStateException("DataStoreManager is not initialized")
}
}
private suspend fun <T> setValue(
key: Preferences.Key<T>,
value: T
) {
context.dataStore.edit { settings ->
settings[key] = value
}
}
private fun <T> getValue(
key: Preferences.Key<T>,
defaultValue: T
): Flow<T> {
return context.dataStore.data.map { pref ->
pref[key] ?: defaultValue
}
}
suspend fun saveToken(token: String) = setValue(
Keys.TOKEN,
token
)
suspend fun savePasscode(passcode: String) = setValue(
Keys.PASSCODE,
passcode
)
suspend fun saveIsLockApp(isLock: Boolean) = setValue(
Keys.IS_LOCK_APP,
isLock
)
suspend fun savePrimaryColor(colorName: String) = setValue(
Keys.PRIMARY_COLOR,
colorName
)
suspend fun saveTheme(theme: ThemeOption) = setValue(
Keys.THEME,
theme.toString()
)
suspend fun saveDynamicColor(dynamicColor: Boolean) = setValue(
Keys.DYNAMIC_COLOR,
dynamicColor
)
fun getToken() = getValue(
Keys.TOKEN,
""
)
fun getPasscode() = getValue(
Keys.PASSCODE,
""
)
fun getIsLockApp() = getValue(
Keys.IS_LOCK_APP,
false
)
fun getPrimaryColor() = getValue(
Keys.PRIMARY_COLOR,
PrimaryColorOption.Blue.name
)
fun getTheme() = getValue(
Keys.THEME,
ThemeOption.SYSTEM.name
)
fun getDynamicColor() = getValue(
Keys.DYNAMIC_COLOR,
false
)
}

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