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

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
)
}

View File

@@ -0,0 +1,22 @@
package com.aiwazian.messenger.services
import android.os.Build
import javax.inject.Inject
class DeviceHelper @Inject constructor() {
fun getDeviceName(): String {
val manufacturer = Build.MANUFACTURER
val model = Build.MODEL
return if (model.startsWith(manufacturer, ignoreCase = true)) {
model
} else {
"$manufacturer $model"
}
}
fun getOsVersion(): String = Build.VERSION.RELEASE
fun getOsName(): String = "Android"
}

View File

@@ -0,0 +1,19 @@
package com.aiwazian.messenger.services
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class DialogController {
var isVisible by mutableStateOf(false)
private set
fun show() {
isVisible = true
}
fun hide() {
isVisible = false
}
}

View File

@@ -0,0 +1,26 @@
package com.aiwazian.messenger.services
import com.aiwazian.messenger.api.RetrofitInstance
import com.aiwazian.messenger.data.FolderInfo
import javax.inject.Inject
class FolderService @Inject constructor() {
suspend fun getAll(): List<FolderInfo>? {
return emptyList()
}
suspend fun save(folderInfo: FolderInfo): Int? {
val request = RetrofitInstance.api.saveFolder(folderInfo)
val savedFolderId = request.body()?.message?.toInt()
return savedFolderId
}
suspend fun remove(folderId: Int): Boolean {
val request = RetrofitInstance.api.deleteFolder(folderId)
return request.isSuccessful
}
}

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