diff --git a/client-mobile/.gitkeep b/client-mobile/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/client-mobile/app/build.gradle.kts b/client-mobile/app/build.gradle.kts
deleted file mode 100644
index 49279eb..0000000
--- a/client-mobile/app/build.gradle.kts
+++ /dev/null
@@ -1,146 +0,0 @@
-plugins {
- id("com.android.application")
- id("org.jetbrains.kotlin.android")
- id("com.google.dagger.hilt.android")
- id("com.google.gms.google-services")
- kotlin("kapt")
-}
-
-android {
- namespace = "ru.knot.messager"
- compileSdk = 34
-
- defaultConfig {
- applicationId = "ru.knot.messager"
- minSdk = 26
- targetSdk = 34
- versionCode = 1
- versionName = "1.0.0"
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables {
- useSupportLibrary = true
- }
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
- }
- }
-
- // Подключаем все наши папки с кодом как sourceSets
- sourceSets {
- getByName("main") {
- java.srcDirs(
- "src/main/kotlin",
- "../auth",
- "../chats",
- "../core",
- "../calls",
- "../stories",
- "../contacts",
- "../profiles",
- "../settings",
- "../navigation"
- )
- }
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
- buildFeatures {
- compose = true
- }
- composeOptions {
- kotlinCompilerExtensionVersion = "1.5.8"
- }
- // Включаем оптимизации компилятора Compose
- kotlinOptions {
- jvmTarget = "17"
- }
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
- }
-}
-
-dependencies {
- // AndroidX & UI
- implementation("androidx.core:core-ktx:1.12.0")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
- implementation("androidx.activity:activity-compose:1.8.1")
- implementation(platform("androidx.compose:compose-bom:2023.10.01"))
- implementation("androidx.compose.ui:ui")
- implementation("androidx.compose.ui:ui-graphics")
- implementation("androidx.compose.ui:ui-tooling-preview")
- implementation("androidx.compose.material3:material3")
- implementation("com.google.android.material:material:1.11.0")
- implementation("androidx.navigation:navigation-compose:2.7.5")
- implementation("androidx.compose.material:material-icons-extended")
-
- // Hilt
- implementation("com.google.dagger:hilt-android:2.48")
- kapt("com.google.dagger:hilt-android-compiler:2.48")
- implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
- implementation("androidx.hilt:hilt-work:1.1.0")
- kapt("androidx.hilt:hilt-compiler:1.1.0")
-
- // Network & SignalR
- implementation("com.squareup.retrofit2:retrofit:2.9.0")
- implementation("com.squareup.retrofit2:converter-gson:2.9.0")
- implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
- implementation("com.microsoft.signalr:signalr:7.0.0")
-
- // WebRTC
- implementation("com.github.webrtc-sdk:android:104.5112.01")
-
- // Media3 (ExoPlayer)
- implementation("androidx.media3:media3-exoplayer:1.2.0")
- implementation("androidx.media3:media3-ui:1.2.0")
- implementation("androidx.media3:media3-common:1.2.0")
-
- // Images & GIF
- implementation("io.coil-kt:coil-compose:2.5.0")
- implementation("io.coil-kt:coil-gif:2.5.0")
- implementation("io.coil-kt:coil-svg:2.5.0")
- implementation("io.coil-kt:coil-video:2.5.0")
-
- // Security
- implementation("androidx.security:security-crypto:1.1.0-alpha06")
-
- // UCrop (Image Cropping)
- implementation("com.github.yalantis:ucrop:2.2.8")
-
- // Firebase (Push Notifications)
- implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
- implementation("com.google.firebase:firebase-messaging-ktx")
- implementation("com.google.firebase:firebase-analytics-ktx")
-
- // Room
- val room_version = "2.6.1"
- implementation("androidx.room:room-runtime:$room_version")
- implementation("androidx.room:room-ktx:$room_version")
- kapt("androidx.room:room-compiler:$room_version")
-
- // WorkManager
- val work_version = "2.9.0"
- implementation("androidx.work:work-runtime-ktx:$work_version")
-
- // Paging 3
- val paging_version = "3.3.0"
- implementation("androidx.paging:paging-runtime-ktx:$paging_version")
- implementation("androidx.paging:paging-compose:$paging_version")
-
- // Testing
- testImplementation("junit:junit:4.13.2")
- androidTestImplementation("androidx.test.ext:junit:1.1.5")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
-}
diff --git a/client-mobile/app/google-services.json b/client-mobile/app/google-services.json
deleted file mode 100644
index cbd9564..0000000
--- a/client-mobile/app/google-services.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "project_info": {
- "project_number": "483917912506",
- "project_id": "knot-bad1a",
- "storage_bucket": "knot-bad1a.firebasestorage.app"
- },
- "client": [
- {
- "client_info": {
- "mobilesdk_app_id": "1:483917912506:android:cd39213364869ef9e82583",
- "android_client_info": {
- "package_name": "ru.knot.messager"
- }
- },
- "oauth_client": [],
- "api_key": [
- {
- "current_key": "AIzaSyBAL_bZJYaa7rGERLX63LeFXz-__JXRWQY"
- }
- ],
- "services": {
- "appinvite_service": {
- "other_platform_oauth_client": []
- }
- }
- }
- ],
- "configuration_version": "1"
-}
\ No newline at end of file
diff --git a/client-mobile/app/src/main/AndroidManifest.xml b/client-mobile/app/src/main/AndroidManifest.xml
deleted file mode 100644
index d06a980..0000000
--- a/client-mobile/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/client-mobile/app/src/main/kotlin/com/knot/messenger/MainActivity.kt b/client-mobile/app/src/main/kotlin/com/knot/messenger/MainActivity.kt
deleted file mode 100644
index 9fa09f0..0000000
--- a/client-mobile/app/src/main/kotlin/com/knot/messenger/MainActivity.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.knot.messenger
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.ui.Modifier
-import dagger.hilt.android.AndroidEntryPoint
-import navigation.AppNavigation
-import core.presentation.theme.KnotTheme
-
-@AndroidEntryPoint
-class MainActivity : ComponentActivity() {
- @javax.inject.Inject
- lateinit var navigationManager: core.utils.NavigationManager
-
- @javax.inject.Inject
- lateinit var signalrNotificationObserver: chats.data.remote.signalr.SignalRNotificationObserver
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- signalrNotificationObserver.start()
-
- intent.getStringExtra("chatId")?.let { chatId ->
- navigationManager.navigateToChat(chatId)
- }
-
- // Request notifications permission for Android 13+
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
- androidx.core.app.ActivityCompat.requestPermissions(
- this,
- arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
- 101
- )
- }
-
- setContent {
- KnotTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background
- ) {
- AppNavigation(navigationManager = navigationManager)
- }
- }
- }
- }
-
- override fun onNewIntent(intent: android.content.Intent?) {
- super.onNewIntent(intent)
- intent?.getStringExtra("chatId")?.let { chatId ->
- navigationManager.navigateToChat(chatId)
- }
- }
-}
diff --git a/client-mobile/app/src/main/kotlin/com/knot/messenger/MainApplication.kt b/client-mobile/app/src/main/kotlin/com/knot/messenger/MainApplication.kt
deleted file mode 100644
index d1415f3..0000000
--- a/client-mobile/app/src/main/kotlin/com/knot/messenger/MainApplication.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.knot.messenger
-
-import android.app.Application
-import androidx.hilt.work.HiltWorkerFactory
-import androidx.work.Configuration
-import coil.ImageLoader
-import coil.ImageLoaderFactory
-import coil.decode.VideoFrameDecoder
-import dagger.hilt.android.HiltAndroidApp
-import javax.inject.Inject
-
-@HiltAndroidApp
-class MainApplication : Application(), ImageLoaderFactory, Configuration.Provider {
-
- @Inject
- lateinit var workerFactory: HiltWorkerFactory
-
- override val workManagerConfiguration: Configuration
- get() = Configuration.Builder()
- .setWorkerFactory(workerFactory)
- .build()
-
- override fun newImageLoader(): ImageLoader {
- return ImageLoader.Builder(this)
- .components {
- add(VideoFrameDecoder.Factory())
- }
- .build()
- }
-}
diff --git a/client-mobile/app/src/main/res/drawable/ic_notification.xml b/client-mobile/app/src/main/res/drawable/ic_notification.xml
deleted file mode 100644
index f99d835..0000000
--- a/client-mobile/app/src/main/res/drawable/ic_notification.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
diff --git a/client-mobile/app/src/main/res/mipmap-hdpi/ic_launcher.png b/client-mobile/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 09264f7..0000000
Binary files a/client-mobile/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/client-mobile/app/src/main/res/mipmap-mdpi/ic_launcher.png b/client-mobile/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index e6ae1b2..0000000
Binary files a/client-mobile/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/client-mobile/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/client-mobile/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 5e867ef..0000000
Binary files a/client-mobile/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/client-mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/client-mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index bc05396..0000000
Binary files a/client-mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/client-mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/client-mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 1af648c..0000000
Binary files a/client-mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/client-mobile/app/src/main/res/values-en/strings.xml b/client-mobile/app/src/main/res/values-en/strings.xml
deleted file mode 100644
index cf42e3d..0000000
--- a/client-mobile/app/src/main/res/values-en/strings.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-
- Knot
- Login
- Register
- Username
- Password
- Display Name
- Settings
- Save
- Back
- Server Connection
- API Base URL
- Server Features
- Stories
- Polls
- Calls
- Groups
- Enabled
- Disabled
- Limits
- Max File Size
- Max Group Members
- Message
- Call
- Block
- Profile
- Confirm Password
- Passwords do not match
- Don\'t have an account? Register
- Already have an account? Login
- An error occurred
- Loading...
- Chats
- Contacts
- Stories
- Create Story
- Type a message...
- Reply to %1$s...
- STORY EDITOR
- PUBLISH
- START CREATION
- TEXT
- CROP
- STICKERS
- BRUSH
- FILTERS
- Remove
- No chats found
- typing...
- Video Call
- Emoji
- Attach
- Message...
- Voice Message
- Send
- Photo
- Video
- Audio
- File
- GIF
- Reply to
- yourself
-
diff --git a/client-mobile/app/src/main/res/values/strings.xml b/client-mobile/app/src/main/res/values/strings.xml
deleted file mode 100644
index d9ea064..0000000
--- a/client-mobile/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-
- Knot
- Войти
- Регистрация
- Имя пользователя
- Пароль
- Отображаемое имя
- Настройки
- Сохранить
- Назад
- Подключение к серверу
- API Base URL
- Функции сервера
- Истории
- Опросы
- Звонки
- Группы
- Включено
- Отключено
- Лимиты
- Макс. размер файла
- Макс. участников в группе
- Сообщение
- Позвонить
- Заблокировать
- Профиль
- Подтвердите пароль
- Пароли не совпадают
- Нет аккаунта? Зарегистрироваться
- Уже есть аккаунт? Войти
- Произошла ошибка
- Загрузка...
- Чаты
- Контакты
- Истории
- Создать историю
- Напишите сообщение...
- Ответить %1$s...
- РЕДАКТОР ИСТОРИЙ
- ОПУБЛИКОВАТЬ
- НАЧАТЬ СОЗДАНИЕ
- ТЕКСТ
- ОБРЕЗКА
- СТИКЕРЫ
- КИСТЬ
- ФИЛЬТРЫ
- Удалить
- Чаты не найдены
- печатает...
- Видеозвонок
- Эмодзи
- Прикрепить
- Сообщение...
- Голосовое сообщение
- Отправить
- Поиск...
- В сети
- Был(а): %1$s
- недавно
- Все
- Онлайн
- Заблокированные
- Медиа
- Уведомления
- Без звука
- Включить звук
- Выйти из аккаунта
- О себе
- Редактировать профиль
- Имя пользователя
- Изменить фото
- Отмена
- Обрезать
- Чаты
- Контакты
- Профиль
- Сохранение...
- Фото
- Видео
- Аудио
- Файл
- GIF
- Ответ
- самому себе
-
diff --git a/client-mobile/app/src/main/res/values/styles.xml b/client-mobile/app/src/main/res/values/styles.xml
deleted file mode 100644
index 3be2708..0000000
--- a/client-mobile/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/client-mobile/app/src/main/res/xml/network_security_config.xml b/client-mobile/app/src/main/res/xml/network_security_config.xml
deleted file mode 100644
index 29e5665..0000000
--- a/client-mobile/app/src/main/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- localhost
- 127.0.0.1
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
-
-
diff --git a/client-mobile/auth/data/remote/api/AuthApi.kt b/client-mobile/auth/data/remote/api/AuthApi.kt
deleted file mode 100644
index 07a5104..0000000
--- a/client-mobile/auth/data/remote/api/AuthApi.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package auth.data.remote.api
-
-import auth.data.remote.dto.AuthRequest
-import auth.data.remote.dto.AuthResponse
-import auth.data.remote.dto.RefreshTokenRequest
-import core.domain.model.ServerConfigModel
-import retrofit2.http.Body
-import retrofit2.http.GET
-import retrofit2.http.Headers
-import retrofit2.http.POST
-
-interface AuthApi {
- @POST("auth/login")
- suspend fun login(@Body request: AuthRequest): AuthResponse
-
- @POST("auth/register")
- suspend fun register(@Body request: AuthRequest): AuthResponse
-
- @POST("auth/refresh")
- suspend fun refreshToken(@Body request: RefreshTokenRequest): AuthResponse
-
- @GET("config")
- @Headers("Cache-Control: no-cache")
- suspend fun getConfig(): ServerConfigModel
-
- @POST("auth/push-token")
- @Headers("Cache-Control: no-cache")
- suspend fun updatePushToken(@Body token: String): Unit
-}
diff --git a/client-mobile/auth/data/remote/dto/AuthDtos.kt b/client-mobile/auth/data/remote/dto/AuthDtos.kt
deleted file mode 100644
index cac9089..0000000
--- a/client-mobile/auth/data/remote/dto/AuthDtos.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package auth.data.remote.dto
-
-import com.google.gson.annotations.SerializedName
-
-data class AuthRequest(
- @SerializedName("userName") val userName: String,
- @SerializedName("password") val password: String
-)
-
-data class AuthResponse(
- @SerializedName("accessToken") val accessToken: String?,
- @SerializedName("refreshToken") val refreshToken: String?,
- @SerializedName("user") val user: UserDto?,
- @SerializedName("userId") val userId: String?,
- @SerializedName("username") val username: String?,
- @SerializedName("displayName") val displayName: String?
-)
-
-data class UserDto(
- @SerializedName("id") val id: String,
- @SerializedName("userName") val userName: String,
- @SerializedName("displayName") val displayName: String?,
- @SerializedName("avatarUrl") val avatarUrl: String?
-)
-
-data class RefreshTokenRequest(
- @SerializedName("refreshToken") val refreshToken: String
-)
diff --git a/client-mobile/auth/data/repository/AuthRepositoryImpl.kt b/client-mobile/auth/data/repository/AuthRepositoryImpl.kt
deleted file mode 100644
index 8b6fae5..0000000
--- a/client-mobile/auth/data/repository/AuthRepositoryImpl.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package auth.data.repository
-
-import auth.data.remote.api.AuthApi
-import auth.data.remote.dto.AuthRequest
-import auth.data.remote.dto.RefreshTokenRequest
-import auth.domain.model.AuthResult
-import auth.domain.repository.AuthRepository
-import core.network.ServerConfig
-import core.security.TokenManager
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import javax.inject.Inject
-
-class AuthRepositoryImpl @Inject constructor(
- private val api: AuthApi,
- private val tokenManager: TokenManager,
- private val serverConfig: ServerConfig
-) : AuthRepository {
-
- private val _isAuthenticated = kotlinx.coroutines.flow.MutableStateFlow(tokenManager.getToken() != null)
-
- override suspend fun login(userName: String, password: String): Result {
- return try {
- val response = api.login(AuthRequest(userName, password))
- val token = response.accessToken ?: return Result.failure(Exception("Token is null"))
- val userId = response.userId ?: ""
-
- tokenManager.saveToken(token, userId, response.refreshToken)
- _isAuthenticated.value = true
- fetchConfig()
- Result.success(
- AuthResult(
- token = token,
- refreshToken = response.refreshToken,
- userId = userId,
- userName = response.username ?: userName,
- displayName = response.displayName ?: response.username ?: userName,
- avatarUrl = null
- )
- )
- } catch (e: Exception) {
- Result.failure(e)
- }
- }
-
- override suspend fun register(userName: String, password: String): Result {
- return try {
- val response = api.register(AuthRequest(userName, password))
- val token = response.accessToken ?: return Result.failure(Exception("Token is null"))
- val userId = response.userId ?: ""
-
- tokenManager.saveToken(token, userId, response.refreshToken)
- _isAuthenticated.value = true
- fetchConfig()
- Result.success(
- AuthResult(
- token = token,
- refreshToken = response.refreshToken,
- userId = userId,
- userName = response.username ?: userName,
- displayName = response.displayName ?: response.username ?: userName,
- avatarUrl = null
- )
- )
- } catch (e: Exception) {
- Result.failure(e)
- }
- }
-
- override suspend fun logout() {
- tokenManager.deleteToken()
- _isAuthenticated.value = false
- }
-
- override fun isAuthenticated(): Boolean {
- return _isAuthenticated.value
- }
-
- override fun isAuthenticatedFlow(): kotlinx.coroutines.flow.StateFlow {
- return _isAuthenticated.asStateFlow()
- }
-
- override suspend fun fetchConfig(): Result {
- return try {
- val config = api.getConfig()
- serverConfig.saveServerConfig(config)
- Result.success(Unit)
- } catch (e: Exception) {
- Result.failure(e)
- }
- }
-
- override suspend fun updatePushToken(token: String) {
- try {
- api.updatePushToken(token)
- } catch (e: Exception) {
- // Silent fail
- }
- }
-
- override suspend fun refreshToken(): Result {
- val currentRefreshToken = tokenManager.getRefreshToken()
- if (currentRefreshToken == null) {
- return Result.failure(Exception("Refresh token is null"))
- }
-
- return try {
- val response = api.refreshToken(RefreshTokenRequest(currentRefreshToken))
- val newAccessToken = response.accessToken ?: return Result.failure(Exception("New access token is null"))
- val newRefreshToken = response.refreshToken
- val userId = response.userId ?: ""
-
- tokenManager.saveToken(newAccessToken, userId, newRefreshToken)
- _isAuthenticated.value = true
-
- Result.success(
- AuthResult(
- token = newAccessToken,
- refreshToken = newRefreshToken,
- userId = userId,
- userName = response.username ?: "",
- displayName = response.displayName ?: "",
- avatarUrl = null
- )
- )
- } catch (e: Exception) {
- Result.failure(e)
- }
- }
-}
diff --git a/client-mobile/auth/di/AuthModule.kt b/client-mobile/auth/di/AuthModule.kt
deleted file mode 100644
index 3c24d67..0000000
--- a/client-mobile/auth/di/AuthModule.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package auth.di
-
-import auth.data.remote.api.AuthApi
-import auth.data.repository.AuthRepositoryImpl
-import auth.domain.repository.AuthRepository
-import core.network.ServerConfig
-import core.security.TokenManager
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import retrofit2.Retrofit
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-object AuthModule {
-
- @Provides
- @Singleton
- fun provideAuthApi(retrofit: Retrofit): AuthApi {
- return retrofit.create(AuthApi::class.java)
- }
-
- @Provides
- @Singleton
- fun provideAuthRepository(
- api: AuthApi,
- tokenManager: TokenManager,
- serverConfig: ServerConfig
- ): AuthRepository {
- return AuthRepositoryImpl(api, tokenManager, serverConfig)
- }
-}
diff --git a/client-mobile/auth/domain/model/AuthModels.kt b/client-mobile/auth/domain/model/AuthModels.kt
deleted file mode 100644
index 06ac946..0000000
--- a/client-mobile/auth/domain/model/AuthModels.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package auth.domain.model
-
-data class AuthResult(
- val token: String,
- val refreshToken: String?,
- val userId: String,
- val userName: String,
- val displayName: String,
- val avatarUrl: String?
-)
diff --git a/client-mobile/auth/domain/repository/AuthRepository.kt b/client-mobile/auth/domain/repository/AuthRepository.kt
deleted file mode 100644
index 781d4a7..0000000
--- a/client-mobile/auth/domain/repository/AuthRepository.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package auth.domain.repository
-
-import auth.domain.model.AuthResult
-
-interface AuthRepository {
- suspend fun login(userName: String, password: String): Result
- suspend fun register(userName: String, password: String): Result
- suspend fun logout()
- suspend fun fetchConfig(): Result
- fun isAuthenticated(): Boolean
- fun isAuthenticatedFlow(): kotlinx.coroutines.flow.StateFlow
- suspend fun updatePushToken(token: String)
- suspend fun refreshToken(): Result
-}
diff --git a/client-mobile/auth/presentation/AuthViewModel.kt b/client-mobile/auth/presentation/AuthViewModel.kt
deleted file mode 100644
index be8ab94..0000000
--- a/client-mobile/auth/presentation/AuthViewModel.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package auth.presentation
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import auth.domain.repository.AuthRepository
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import com.google.firebase.messaging.FirebaseMessaging
-import javax.inject.Inject
-
-data class AuthState(
- val isLoading: Boolean = false,
- val error: String? = null,
- val isAuthenticated: Boolean = false
-)
-
-@HiltViewModel
-class AuthViewModel @Inject constructor(
- private val repository: AuthRepository
-) : ViewModel() {
-
- private val _state = MutableStateFlow(AuthState(isAuthenticated = repository.isAuthenticated()))
- val state: StateFlow = _state.asStateFlow()
-
- init {
- viewModelScope.launch {
- repository.isAuthenticatedFlow().collect { authenticated ->
- _state.update { it.copy(isAuthenticated = authenticated) }
- if (authenticated) {
- updatePushToken()
- }
- }
- }
- }
-
- fun checkAuth() {
- _state.update { it.copy(isAuthenticated = repository.isAuthenticated()) }
- }
-
- fun login(userName: String, password: String) {
- viewModelScope.launch {
- _state.update { it.copy(isLoading = true, error = null) }
- repository.login(userName, password)
- .onSuccess {
- _state.update { it.copy(isLoading = false, isAuthenticated = true) }
- updatePushToken()
- }
- .onFailure { e ->
- _state.update { it.copy(isLoading = false, error = e.message) }
- }
- }
- }
-
- fun register(userName: String, password: String) {
- viewModelScope.launch {
- _state.update { it.copy(isLoading = true, error = null) }
- repository.register(userName, password)
- .onSuccess {
- _state.update { it.copy(isLoading = false, isAuthenticated = true) }
- updatePushToken()
- }
- .onFailure { e ->
- _state.update { it.copy(isLoading = false, error = e.message) }
- }
- }
- }
-
- fun refreshToken() {
- viewModelScope.launch {
- repository.refreshToken()
- .onSuccess {
- // Token refreshed successfully
- }
- .onFailure {
- // Refresh failed, will trigger logout via AuthInterceptor
- }
- }
- }
-
- private fun updatePushToken() {
- FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
- if (task.isSuccessful) {
- val token = task.result
- viewModelScope.launch {
- repository.updatePushToken(token)
- }
- }
- }
- }
-}
diff --git a/client-mobile/auth/presentation/LoginScreen.kt b/client-mobile/auth/presentation/LoginScreen.kt
deleted file mode 100644
index a9865bc..0000000
--- a/client-mobile/auth/presentation/LoginScreen.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package auth.presentation
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.unit.dp
-import ru.knot.messager.R
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun LoginScreen(
- viewModel: AuthViewModel,
- onNavigateToRegister: () -> Unit,
- onNavigateToSettings: () -> Unit,
- onLoginSuccess: () -> Unit
-) {
- val state by viewModel.state.collectAsState()
- var userName by remember { mutableStateOf("") }
- var password by remember { mutableStateOf("") }
-
- LaunchedEffect(state.isAuthenticated) {
- if (state.isAuthenticated) {
- onLoginSuccess()
- }
- }
-
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text(stringResource(R.string.login)) },
- actions = {
- IconButton(onClick = onNavigateToSettings) {
- Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.settings))
- }
- }
- )
- }
- ) { paddingValues ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- OutlinedTextField(
- value = userName,
- onValueChange = { userName = it },
- label = { Text(stringResource(R.string.username)) },
- modifier = Modifier.fillMaxWidth(),
- singleLine = true
- )
- Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextField(
- value = password,
- onValueChange = { password = it },
- label = { Text(stringResource(R.string.password)) },
- modifier = Modifier.fillMaxWidth(),
- visualTransformation = PasswordVisualTransformation(),
- singleLine = true
- )
- Spacer(modifier = Modifier.height(16.dp))
-
- if (state.isLoading) {
- CircularProgressIndicator()
- } else {
- Button(
- onClick = { viewModel.login(userName, password) },
- modifier = Modifier.fillMaxWidth(),
- enabled = userName.isNotBlank() && password.isNotBlank()
- ) {
- Text(stringResource(R.string.login))
- }
- TextButton(onClick = onNavigateToRegister) {
- Text(stringResource(R.string.no_account_register))
- }
- }
-
- if (state.error != null) {
- Text(
- text = state.error!!,
- color = MaterialTheme.colorScheme.error,
- modifier = Modifier.padding(top = 8.dp)
- )
- }
- }
- }
-}
diff --git a/client-mobile/auth/presentation/RegisterScreen.kt b/client-mobile/auth/presentation/RegisterScreen.kt
deleted file mode 100644
index 61698f5..0000000
--- a/client-mobile/auth/presentation/RegisterScreen.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-package auth.presentation
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.unit.dp
-import ru.knot.messager.R
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun RegisterScreen(
- viewModel: AuthViewModel,
- onNavigateToLogin: () -> Unit,
- onRegisterSuccess: () -> Unit
-) {
- val state by viewModel.state.collectAsState()
- var userName by remember { mutableStateOf("") }
- var password by remember { mutableStateOf("") }
- var confirmPassword by remember { mutableStateOf("") }
- var errorMessage by remember { mutableStateOf(null) }
-
- val passwordsNotMatchMsg = stringResource(R.string.passwords_not_match)
-
- LaunchedEffect(state.isAuthenticated) {
- if (state.isAuthenticated) {
- onRegisterSuccess()
- }
- }
-
- Scaffold(
- topBar = {
- TopAppBar(title = { Text(stringResource(R.string.register)) })
- }
- ) { paddingValues ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- OutlinedTextField(
- value = userName,
- onValueChange = { userName = it },
- label = { Text(stringResource(R.string.username)) },
- modifier = Modifier.fillMaxWidth(),
- singleLine = true
- )
- Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextField(
- value = password,
- onValueChange = { password = it },
- label = { Text(stringResource(R.string.password)) },
- modifier = Modifier.fillMaxWidth(),
- visualTransformation = PasswordVisualTransformation(),
- singleLine = true
- )
- Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextField(
- value = confirmPassword,
- onValueChange = { confirmPassword = it },
- label = { Text(stringResource(R.string.confirm_password)) },
- modifier = Modifier.fillMaxWidth(),
- visualTransformation = PasswordVisualTransformation(),
- singleLine = true
- )
- Spacer(modifier = Modifier.height(16.dp))
-
- if (state.isLoading) {
- CircularProgressIndicator()
- } else {
- Button(
- onClick = {
- if (password == confirmPassword) {
- errorMessage = null
- viewModel.register(userName, password)
- } else {
- errorMessage = passwordsNotMatchMsg
- }
- },
- modifier = Modifier.fillMaxWidth(),
- enabled = userName.isNotBlank() && password.isNotBlank() && confirmPassword.isNotBlank()
- ) {
- Text(stringResource(R.string.register))
- }
- TextButton(onClick = onNavigateToLogin) {
- Text(stringResource(R.string.already_have_account))
- }
- }
-
- val displayError = state.error ?: errorMessage
- if (displayError != null) {
- Text(
- text = displayError,
- color = MaterialTheme.colorScheme.error,
- modifier = Modifier.padding(top = 8.dp)
- )
- }
- }
- }
-}
diff --git a/client-mobile/build.gradle.kts b/client-mobile/build.gradle.kts
deleted file mode 100644
index 70ac46c..0000000
--- a/client-mobile/build.gradle.kts
+++ /dev/null
@@ -1,8 +0,0 @@
-// Top-level build file
-plugins {
- id("com.android.application") version "8.2.0" apply false
- id("com.android.library") version "8.2.0" apply false
- id("org.jetbrains.kotlin.android") version "1.9.22" apply false
- id("com.google.dagger.hilt.android") version "2.48" apply false
- id("com.google.gms.google-services") version "4.4.0" apply false
-}
diff --git a/client-mobile/calls/data/remote/CallAudioManager.kt b/client-mobile/calls/data/remote/CallAudioManager.kt
deleted file mode 100644
index 6d8c01f..0000000
--- a/client-mobile/calls/data/remote/CallAudioManager.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package calls.data.remote
-
-import android.content.Context
-import android.media.AudioAttributes
-import android.media.AudioFocusRequest
-import android.media.AudioManager
-import android.os.Build
-
-class CallAudioManager(private val context: Context) {
- private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
- private var originalMode: Int = AudioManager.MODE_NORMAL
- private var originalIsSpeakerphoneOn: Boolean = false
-
- fun startCallMode(isVideoCall: Boolean) {
- originalMode = audioManager.mode
- originalIsSpeakerphoneOn = audioManager.isSpeakerphoneOn
-
- audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
- setSpeakerphoneOn(isVideoCall)
-
- // Запрашиваем фокус аудио
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val playbackAttributes = AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
- .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
- .build()
- val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
- .setAudioAttributes(playbackAttributes)
- .build()
- audioManager.requestAudioFocus(focusRequest)
- }
- }
-
- fun setSpeakerphoneOn(on: Boolean) {
- audioManager.isSpeakerphoneOn = on
- }
-
- fun stopCallMode() {
- audioManager.mode = originalMode
- audioManager.isSpeakerphoneOn = originalIsSpeakerphoneOn
- audioManager.abandonAudioFocus(null)
- }
-}
diff --git a/client-mobile/calls/data/remote/GroupWebRtcManager.kt b/client-mobile/calls/data/remote/GroupWebRtcManager.kt
deleted file mode 100644
index ecfdcb0..0000000
--- a/client-mobile/calls/data/remote/GroupWebRtcManager.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package calls.data.remote
-
-import android.content.Context
-import org.webrtc.*
-import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class GroupWebRtcManager @Inject constructor(private val context: Context) {
- private val peerConnections = ConcurrentHashMap()
- private val factory: PeerConnectionFactory by lazy { createFactory() }
-
- private fun createFactory(): PeerConnectionFactory {
- PeerConnectionFactory.initialize(
- PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()
- )
- return PeerConnectionFactory.builder()
- .setVideoEncoderFactory(DefaultVideoEncoderFactory(EglBase.create().eglBaseContext, true, true))
- .setVideoDecoderFactory(DefaultVideoDecoderFactory(EglBase.create().eglBaseContext))
- .createPeerConnectionFactory()
- }
-
- fun addParticipant(userId: String, observer: PeerConnection.Observer): PeerConnection? {
- val iceServers = listOf(
- PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
- )
- val pc = factory.createPeerConnection(iceServers, observer)
- if (pc != null) {
- peerConnections[userId] = pc
- }
- return pc
- }
-
- fun removeParticipant(userId: String) {
- peerConnections[userId]?.dispose()
- peerConnections.remove(userId)
- }
-
- fun getPeerConnection(userId: String): PeerConnection? = peerConnections[userId]
-
- fun closeAll() {
- peerConnections.values.forEach { it.dispose() }
- peerConnections.clear()
- }
-}
diff --git a/client-mobile/calls/data/remote/WebRtcManager.kt b/client-mobile/calls/data/remote/WebRtcManager.kt
deleted file mode 100644
index 5ca37ea..0000000
--- a/client-mobile/calls/data/remote/WebRtcManager.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package calls.data.remote
-
-import android.content.Context
-import org.webrtc.*
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class WebRtcManager @Inject constructor(private val context: Context) {
- private var peerConnection: PeerConnection? = null
- private val factory: PeerConnectionFactory by lazy { createFactory() }
-
- // Аудио и видео источники
- private val videoSource by lazy { factory.createVideoSource(false) }
- private val audioSource by lazy { factory.createAudioSource(MediaConstraints()) }
-
- private fun createFactory(): PeerConnectionFactory {
- PeerConnectionFactory.initialize(
- PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()
- )
- return PeerConnectionFactory.builder()
- .setVideoEncoderFactory(DefaultVideoEncoderFactory(EglBase.create().eglBaseContext, true, true))
- .setVideoDecoderFactory(DefaultVideoDecoderFactory(EglBase.create().eglBaseContext))
- .createPeerConnectionFactory()
- }
-
- fun initializePeerConnection(observer: PeerConnection.Observer) {
- val iceServers = listOf(
- PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
- )
- peerConnection = factory.createPeerConnection(iceServers, observer)
- }
-
- fun createOffer(observer: SdpObserver) {
- peerConnection?.createOffer(observer, MediaConstraints())
- }
-
- fun setRemoteDescription(sdp: String, type: SessionDescription.Type, observer: SdpObserver) {
- peerConnection?.setRemoteDescription(observer, SessionDescription(type, sdp))
- }
-
- fun addIceCandidate(candidate: IceCandidate) {
- peerConnection?.addIceCandidate(candidate)
- }
-
- fun close() {
- peerConnection?.dispose()
- peerConnection = null
- }
-}
diff --git a/client-mobile/calls/presentation/CallScreen.kt b/client-mobile/calls/presentation/CallScreen.kt
deleted file mode 100644
index 0505c90..0000000
--- a/client-mobile/calls/presentation/CallScreen.kt
+++ /dev/null
@@ -1,148 +0,0 @@
-package calls.presentation
-
-import androidx.compose.animation.*
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.*
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import coil.compose.AsyncImage
-import core.presentation.components.AppAvatar
-import ru.knot.messager.R
-
-@Composable
-fun CallScreen(
- viewModel: CallViewModel,
- onBack: () -> Unit
-) {
- val state by viewModel.state.collectAsState()
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(
- Brush.verticalGradient(
- colors = listOf(
- Color(0xFF0F0F10),
- Color(0xFF161618),
- Color(0xFF6366F1).copy(alpha = 0.2f)
- )
- )
- )
- ) {
- // Контент звонка (Аватар или Видео)
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(top = 100.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- AppAvatar(
- url = state.callerAvatar,
- name = state.callerName,
- size = 140.dp
- )
-
- Spacer(modifier = Modifier.height(24.dp))
-
- Text(
- text = state.callerName,
- style = MaterialTheme.typography.headlineMedium,
- color = Color.White,
- fontWeight = FontWeight.Bold
- )
-
- Text(
- text = when (state.status) {
- CallStatus.INCOMING -> "Входящий звонок..."
- CallStatus.OUTGOING -> "Вызов..."
- CallStatus.CONNECTED -> "00:00" // TODO: Timer
- CallStatus.ENDED -> "Звонок завершен"
- else -> ""
- },
- style = MaterialTheme.typography.bodyLarge,
- color = Color.White.copy(alpha = 0.7f)
- )
- }
-
- // Кнопки управления (Внизу)
- Box(
- modifier = Modifier
- .align(Alignment.BottomCenter)
- .padding(bottom = 60.dp)
- .fillMaxWidth(),
- contentAlignment = Alignment.Center
- ) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(32.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (state.status == CallStatus.INCOMING) {
- // Кнопка отклонить
- CallActionButton(
- icon = Icons.Default.CallEnd,
- backgroundColor = Color.Red,
- onClick = { viewModel.endCall(); onBack() }
- )
- // Кнопка принять
- CallActionButton(
- icon = Icons.Default.Call,
- backgroundColor = Color(0xFF10B981), // Green
- onClick = { viewModel.acceptCall() }
- )
- } else {
- // Стандартные кнопки во время разговора
- IconButton(
- onClick = { /* viewModel.toggleMic() */ },
- modifier = Modifier.size(56.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.1f))
- ) {
- Icon(Icons.Default.Mic, contentDescription = null, tint = Color.White)
- }
-
- CallActionButton(
- icon = Icons.Default.CallEnd,
- backgroundColor = Color.Red,
- onClick = { viewModel.endCall(); onBack() }
- )
-
- IconButton(
- onClick = { /* viewModel.toggleSpeaker() */ },
- modifier = Modifier.size(56.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.1f))
- ) {
- Icon(Icons.Default.VolumeUp, contentDescription = null, tint = Color.White)
- }
- }
- }
- }
- }
-}
-
-@Composable
-fun CallActionButton(
- icon: androidx.compose.ui.graphics.vector.ImageVector,
- backgroundColor: Color,
- onClick: () -> Unit
-) {
- FloatingActionButton(
- onClick = onClick,
- containerColor = backgroundColor,
- contentColor = Color.White,
- shape = CircleShape,
- modifier = Modifier.size(64.dp)
- ) {
- Icon(icon, contentDescription = null, modifier = Modifier.size(32.dp))
- }
-}
diff --git a/client-mobile/calls/presentation/CallViewModel.kt b/client-mobile/calls/presentation/CallViewModel.kt
deleted file mode 100644
index 8ba9fc8..0000000
--- a/client-mobile/calls/presentation/CallViewModel.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-package calls.presentation
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import calls.data.remote.WebRtcManager
-import chats.data.remote.signalr.ChatEvent
-import chats.data.remote.signalr.ChatHubClient
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.*
-import kotlinx.coroutines.launch
-import org.webrtc.*
-import javax.inject.Inject
-
-enum class CallStatus { IDLE, INCOMING, OUTGOING, CONNECTED, ENDED }
-
-data class CallState(
- val status: CallStatus = CallStatus.IDLE,
- val chatId: String? = null,
- val callerName: String = "",
- val callerAvatar: String? = null,
- val isMuted: Boolean = false,
- val isSpeakerOn: Boolean = false,
- val localVideoTrack: VideoTrack? = null,
- val remoteVideoTrack: VideoTrack? = null
-)
-
-@HiltViewModel
-class CallViewModel @Inject constructor(
- private val webRtcManager: WebRtcManager,
- private val signalrClient: ChatHubClient
-) : ViewModel() {
-
- private val _state = MutableStateFlow(CallState())
- val state: StateFlow = _state.asStateFlow()
-
- init {
- observeSignaling()
- }
-
- private fun observeSignaling() {
- signalrClient.events
- .onEach { event ->
- when (event) {
- is ChatEvent.CallIncoming -> onIncomingCall(event)
- is ChatEvent.CallAnswered -> onCallAnswered(event)
- is ChatEvent.IceCandidateReceived -> onIceCandidate(event)
- is ChatEvent.CallEnded -> onCallEnded()
- else -> Unit
- }
- }
- .launchIn(viewModelScope)
- }
-
- private fun onIncomingCall(event: ChatEvent.CallIncoming) {
- _state.update { it.copy(
- status = CallStatus.INCOMING,
- chatId = event.chatId,
- callerName = "User ${event.from}" // TODO: Load actual user info
- ) }
- // Set remote description from offer
- webRtcManager.setRemoteDescription(event.offer, SessionDescription.Type.OFFER, object : SdpObserver {
- override fun onCreateSuccess(p0: SessionDescription?) {}
- override fun onSetSuccess() {}
- override fun onCreateFailure(p0: String?) {}
- override fun onSetFailure(p0: String?) {}
- })
- }
-
- private fun onCallAnswered(event: ChatEvent.CallAnswered) {
- _state.update { it.copy(status = CallStatus.CONNECTED) }
- webRtcManager.setRemoteDescription(event.answer, SessionDescription.Type.ANSWER, object : SdpObserver {
- override fun onCreateSuccess(p0: SessionDescription?) {}
- override fun onSetSuccess() {}
- override fun onCreateFailure(p0: String?) {}
- override fun onSetFailure(p0: String?) {}
- })
- }
-
- private fun onIceCandidate(event: ChatEvent.IceCandidateReceived) {
- // Parse candidate JSON and add to peer connection
- // webRtcManager.addIceCandidate(...)
- }
-
- private fun onCallEnded() {
- _state.update { it.copy(status = CallStatus.ENDED) }
- webRtcManager.close()
- }
-
- fun acceptCall() {
- val chatId = _state.value.chatId ?: return
- // Create answer and send via SignalR
- }
-
- fun endCall() {
- val chatId = _state.value.chatId ?: return
- // Send call_end via SignalR
- onCallEnded()
- }
-}
diff --git a/client-mobile/calls/presentation/GroupCallViewModel.kt b/client-mobile/calls/presentation/GroupCallViewModel.kt
deleted file mode 100644
index cd5ff8e..0000000
--- a/client-mobile/calls/presentation/GroupCallViewModel.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package calls.presentation
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import chats.data.remote.signalr.ChatHubClient
-import chats.data.remote.signalr.ChatEvent
-import calls.data.remote.GroupWebRtcManager
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.*
-import kotlinx.coroutines.launch
-import org.webrtc.*
-import javax.inject.Inject
-
-data class ParticipantState(
- val userId: String,
- val videoTrack: VideoTrack? = null,
- val isAudioMuted: Boolean = false,
- val isVideoDisabled: Boolean = false
-)
-
-data class GroupCallState(
- val chatId: String? = null,
- val participants: Map = emptyMap(),
- val isMicEnabled: Boolean = true,
- val isCameraEnabled: Boolean = true
-)
-
-@HiltViewModel
-class GroupCallViewModel @Inject constructor(
- private val webRtcManager: GroupWebRtcManager,
- private val signalrClient: ChatHubClient
-) : ViewModel() {
-
- private val _state = MutableStateFlow(GroupCallState())
- val state: StateFlow = _state.asStateFlow()
-
- init {
- observeSignalREvents()
- }
-
- private fun observeSignalREvents() {
- signalrClient.events.onEach { event ->
- when (event) {
- is ChatEvent.GroupCallUserJoined -> handleUserJoined(event.userId)
- is ChatEvent.GroupCallUserLeft -> handleUserLeft(event.userId)
- is ChatEvent.GroupCallOffer -> handleOffer(event.from, event.offer)
- is ChatEvent.GroupCallAnswer -> handleAnswer(event.from, event.answer)
- // Дополнительные обработчики ICE кандидатов и т.д.
- else -> Unit
- }
- }.launchIn(viewModelScope)
- }
-
- private fun handleUserJoined(userId: String) {
- // Создаем PeerConnection для нового участника
- // Логика идентична портированному CallModal.tsx
- _state.update { it.copy(participants = it.participants + (userId to ParticipantState(userId))) }
- }
-
- private fun handleUserLeft(userId: String) {
- webRtcManager.removeParticipant(userId)
- _state.update { it.copy(participants = it.participants - userId) }
- }
-
- private fun handleOffer(from: String, sdp: String) {
- // Установка RemoteDescription и создание Answer
- }
-
- private fun handleAnswer(from: String, sdp: String) {
- // Установка RemoteDescription
- }
-
- fun toggleMic() {
- _state.update { it.copy(isMicEnabled = !it.isMicEnabled) }
- // Логика управления AudioTrack
- }
-
- fun toggleCamera() {
- _state.update { it.copy(isCameraEnabled = !it.isCameraEnabled) }
- // Логика управления VideoTrack
- }
-
- override fun onCleared() {
- super.onCleared()
- webRtcManager.closeAll()
- }
-}
diff --git a/client-mobile/calls/presentation/components/VideoGrid.kt b/client-mobile/calls/presentation/components/VideoGrid.kt
deleted file mode 100644
index 5d7fa6a..0000000
--- a/client-mobile/calls/presentation/components/VideoGrid.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package calls.presentation.components
-
-import android.view.ViewGroup
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
-import org.webrtc.EglBase
-import org.webrtc.SurfaceViewRenderer
-import org.webrtc.VideoTrack
-
-@Composable
-fun VideoGrid(
- participants: Map,
- localVideoTrack: VideoTrack?
-) {
- val eglBaseContext = remember { EglBase.create().eglBaseContext }
- val allVideoTracks = remember(participants, localVideoTrack) {
- listOfNotNull(localVideoTrack) + participants.values.filterNotNull()
- }
-
- LazyVerticalGrid(
- columns = GridCells.Fixed(if (allVideoTracks.size <= 2) 1 else 2),
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(8.dp)
- ) {
- items(allVideoTracks) { track ->
- Box(
- modifier = Modifier
- .padding(4.dp)
- .fillMaxWidth()
- .aspectRatio(if (allVideoTracks.size == 1) 0.6f else 1f)
- ) {
- VideoRenderer(videoTrack = track, eglBaseContext = eglBaseContext)
- }
- }
- }
-}
-
-@Composable
-fun VideoRenderer(
- videoTrack: VideoTrack,
- eglBaseContext: EglBase.Context
-) {
- AndroidView(
- factory = { context ->
- SurfaceViewRenderer(context).apply {
- init(eglBaseContext, null)
- layoutParams = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
- )
- }
- },
- update = { view ->
- videoTrack.addSink(view)
- },
- onRelease = { view ->
- videoTrack.removeSink(view)
- view.release()
- }
- )
-}
diff --git a/client-mobile/chats/data/local/dao/ChatDao.kt b/client-mobile/chats/data/local/dao/ChatDao.kt
deleted file mode 100644
index b28b825..0000000
--- a/client-mobile/chats/data/local/dao/ChatDao.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package chats.data.local.dao
-
-import androidx.room.*
-import chats.data.local.database.ChatEntity
-import kotlinx.coroutines.flow.Flow
-
-/**
- * DAO для операций с чатами в Room Database
- */
-@Dao
-interface ChatDao {
-
- @Query("SELECT * FROM chats ORDER BY lastMessageTimestamp DESC")
- fun getAllChats(): Flow>
-
- @Query("SELECT * FROM chats WHERE remoteId = :remoteId LIMIT 1")
- suspend fun getChatByRemoteId(remoteId: String): ChatEntity?
-
- @Query("SELECT * FROM chats WHERE localId = :localId LIMIT 1")
- suspend fun getChatByLocalId(localId: String): ChatEntity?
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertChat(chat: ChatEntity)
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertChats(chats: List)
-
- @Update
- suspend fun updateChat(chat: ChatEntity)
-
- @Query("UPDATE chats SET lastMessageText = :lastMessageText, lastMessageTimestamp = :timestamp WHERE remoteId = :chatId")
- suspend fun updateLastMessage(chatId: String, lastMessageText: String?, timestamp: Long?)
-
- @Query("UPDATE chats SET unreadCount = :count WHERE remoteId = :chatId")
- suspend fun updateUnreadCount(chatId: String, count: Int)
-
- @Delete
- suspend fun deleteChat(chat: ChatEntity)
-
- @Query("DELETE FROM chats WHERE remoteId = :remoteId")
- suspend fun deleteChatByRemoteId(remoteId: String)
-
- @Query("DELETE FROM chats")
- suspend fun deleteAllChats()
-}
diff --git a/client-mobile/chats/data/local/dao/MessageDao.kt b/client-mobile/chats/data/local/dao/MessageDao.kt
deleted file mode 100644
index 91beb19..0000000
--- a/client-mobile/chats/data/local/dao/MessageDao.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package chats.data.local.dao
-
-import androidx.room.*
-import chats.data.local.database.MessageEntity
-import chats.domain.model.MessageStatus
-import kotlinx.coroutines.flow.Flow
-
-/**
- * DAO для операций с сообщениями в Room Database
- */
-@Dao
-interface MessageDao {
-
- @Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId ASC")
- fun getMessagesByChatId(chatId: String): Flow>
-
- /**
- * Пагинированная загрузка сообщений для Paging 3
- * sequenceId увеличивается от старых к новым, поэтому ORDER BY DESC для отображения newest внизу
- */
- @Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId DESC LIMIT :limit OFFSET :offset")
- suspend fun getMessagesPaged(chatId: String, offset: Int, limit: Int): List
-
- @Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId DESC LIMIT 1")
- suspend fun getLastMessage(chatId: String): MessageEntity?
-
- @Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId ASC LIMIT 1")
- suspend fun getFirstMessage(chatId: String): MessageEntity?
-
- @Query("SELECT * FROM messages WHERE chatId = :chatId AND sequenceId > :afterSequenceId ORDER BY sequenceId ASC LIMIT :limit")
- suspend fun getMessagesAfter(chatId: String, afterSequenceId: Long, limit: Int): List
-
- @Query("SELECT * FROM messages WHERE chatId = :chatId AND sequenceId < :beforeSequenceId ORDER BY sequenceId DESC LIMIT :limit")
- suspend fun getMessagesBefore(chatId: String, beforeSequenceId: Long, limit: Int): List
-
- @Query("SELECT * FROM messages WHERE localId = :localId")
- suspend fun getMessageByLocalId(localId: String): MessageEntity?
-
- @Query("SELECT * FROM messages WHERE serverId = :serverId")
- suspend fun getMessageByServerId(serverId: String): MessageEntity?
-
- @Query("SELECT * FROM messages WHERE status IN (:statuses) AND chatId = :chatId")
- suspend fun getMessagesByStatus(chatId: String, statuses: List): List
-
- @Query("SELECT * FROM messages WHERE status = :status")
- fun getMessagesByStatusFlow(status: MessageStatus): Flow>
-
- @Query("SELECT * FROM messages WHERE status IN (:statuses)")
- suspend fun getPendingMessages(statuses: List): List
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertMessage(message: MessageEntity)
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertMessages(messages: List)
-
- @Update
- suspend fun updateMessage(message: MessageEntity)
-
- @Delete
- suspend fun deleteMessage(message: MessageEntity)
-
- @Query("DELETE FROM messages WHERE localId = :localId")
- suspend fun deleteMessageByLocalId(localId: String)
-
- @Query("DELETE FROM messages WHERE chatId = :chatId")
- suspend fun deleteMessagesByChatId(chatId: String)
-
- @Query("UPDATE messages SET status = :status, updatedAtMillis = :updatedAtMillis WHERE localId = :localId")
- suspend fun updateMessageStatus(localId: String, status: MessageStatus, updatedAtMillis: Long)
-
- @Query("UPDATE messages SET serverId = :serverId, status = :status, updatedAtMillis = :updatedAtMillis WHERE localId = :localId")
- suspend fun updateMessageWithServerId(localId: String, serverId: String, status: MessageStatus, updatedAtMillis: Long)
-
- @Query("UPDATE messages SET status = :status, errorMessage = :errorMessage, retryCount = retryCount + 1, updatedAtMillis = :updatedAtMillis WHERE localId = :localId")
- suspend fun updateMessageError(localId: String, status: MessageStatus, errorMessage: String, updatedAtMillis: Long)
-
- @Query("SELECT COUNT(*) FROM messages WHERE chatId = :chatId AND status IN (:statuses)")
- suspend fun getUnsentCount(chatId: String, statuses: List): Int
-
- @Query("SELECT * FROM messages WHERE chatId = :chatId AND status = :status LIMIT 1")
- suspend fun getFirstMessageByStatus(chatId: String, status: MessageStatus): MessageEntity?
-}
diff --git a/client-mobile/chats/data/local/dao/UserProfileDao.kt b/client-mobile/chats/data/local/dao/UserProfileDao.kt
deleted file mode 100644
index 41ec972..0000000
--- a/client-mobile/chats/data/local/dao/UserProfileDao.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package chats.data.local.dao
-
-import androidx.room.*
-import chats.data.local.database.UserProfileEntity
-import kotlinx.coroutines.flow.Flow
-
-/**
- * DAO для операций с профилями пользователей в Room Database
- */
-@Dao
-interface UserProfileDao {
-
- @Query("SELECT * FROM user_profile WHERE userId = :userId LIMIT 1")
- suspend fun getUserById(userId: String): UserProfileEntity?
-
- @Query("SELECT * FROM user_profile WHERE userId = :userId")
- fun getUserByIdFlow(userId: String): Flow
-
- @Query("SELECT * FROM user_profile")
- fun getAllUsers(): Flow>
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertUser(user: UserProfileEntity)
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertUsers(users: List)
-
- @Update
- suspend fun updateUser(user: UserProfileEntity)
-
- @Delete
- suspend fun deleteUser(user: UserProfileEntity)
-
- @Query("DELETE FROM user_profile WHERE userId = :userId")
- suspend fun deleteUserById(userId: String)
-}
diff --git a/client-mobile/chats/data/local/database/AppDatabase.kt b/client-mobile/chats/data/local/database/AppDatabase.kt
deleted file mode 100644
index 346fec5..0000000
--- a/client-mobile/chats/data/local/database/AppDatabase.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package chats.data.local.database
-
-import androidx.room.Database
-import androidx.room.RoomDatabase
-import androidx.room.TypeConverters
-import chats.data.local.dao.ChatDao
-import chats.data.local.dao.MessageDao
-import chats.data.local.dao.UserProfileDao
-
-/**
- * Room Database для локального хранения данных мессенджера
- * Реализует паттерн Single Source of Truth
- */
-@Database(
- entities = [
- MessageEntity::class,
- ChatEntity::class,
- UserProfileEntity::class
- ],
- version = 2,
- exportSchema = false
-)
-@TypeConverters(MessageStatusConverter::class)
-abstract class AppDatabase : RoomDatabase() {
-
- abstract fun messageDao(): MessageDao
- abstract fun chatDao(): ChatDao
- abstract fun userProfileDao(): UserProfileDao
-
- companion object {
- const val DATABASE_NAME = "knot_messenger.db"
- }
-}
diff --git a/client-mobile/chats/data/local/database/ChatEntity.kt b/client-mobile/chats/data/local/database/ChatEntity.kt
deleted file mode 100644
index 250d5a8..0000000
--- a/client-mobile/chats/data/local/database/ChatEntity.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package chats.data.local.database
-
-import androidx.room.Entity
-import androidx.room.Index
-import androidx.room.PrimaryKey
-
-/**
- * Entity для хранения чатов в локальной БД Room
- */
-@Entity(
- tableName = "chats",
- indices = [
- Index(value = ["remoteId"], unique = true)
- ]
-)
-data class ChatEntity(
- @PrimaryKey(autoGenerate = false)
- val localId: String,
-
- val remoteId: String?, // ID с сервера
-
- val type: String, // PRIVATE, GROUP, CHANNEL
-
- val name: String,
-
- val avatar: String? = null,
-
- val unreadCount: Int = 0,
-
- val lastMessageId: String? = null,
-
- val lastMessageText: String? = null,
-
- val lastMessageTimestamp: Long? = null,
-
- val lastMessageJson: String? = null,
-
- val updatedAtMillis: Long = System.currentTimeMillis()
-)
diff --git a/client-mobile/chats/data/local/database/MessageEntity.kt b/client-mobile/chats/data/local/database/MessageEntity.kt
deleted file mode 100644
index f458253..0000000
--- a/client-mobile/chats/data/local/database/MessageEntity.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package chats.data.local.database
-
-import androidx.room.Entity
-import androidx.room.Index
-import androidx.room.PrimaryKey
-import chats.domain.model.MessageStatus
-
-/**
- * Entity для хранения сообщений в локальной БД Room
- * Используется для офлайн-режима и Single Source of Truth паттерна
- */
-@Entity(
- tableName = "messages",
- indices = [
- Index(value = ["chatId", "sequenceId"]),
- Index(value = ["localId"], unique = true),
- Index(value = ["serverId"]),
- Index(value = ["status"])
- ]
-)
-data class MessageEntity(
- @PrimaryKey(autoGenerate = false)
- val localId: String, // UUID генерируется на клиенте при создании
-
- val serverId: String?, // null для неотправленных сообщений
-
- val idempotencyKey: String, // UUID v4 для идемпотентности
-
- val chatId: String,
-
- val senderId: String,
-
- val senderName: String,
-
- val senderAvatar: String? = null,
-
- val content: String?,
-
- val sequenceId: Long,
-
- val createdAt: String, // ISO-8601 формат
-
- val mediaType: String,
-
- val mediaJson: String, // JSON список медиа (для Room)
-
- val reactionsJson: String = "{}", // JSON map эмодзи -> count
-
- val status: MessageStatus,
-
- val isPinned: Boolean = false,
-
- val isForwarded: Boolean = false,
-
- val forwardedFromName: String? = null,
-
- val replyToServerId: String? = null,
-
- val errorMessage: String? = null,
-
- val retryCount: Int = 0,
-
- val createdAtMillis: Long = 0,
-
- val updatedAtMillis: Long = 0
-)
diff --git a/client-mobile/chats/data/local/database/MessageStatusConverter.kt b/client-mobile/chats/data/local/database/MessageStatusConverter.kt
deleted file mode 100644
index 1977145..0000000
--- a/client-mobile/chats/data/local/database/MessageStatusConverter.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package chats.data.local.database
-
-import androidx.room.TypeConverter
-import chats.domain.model.MessageStatus
-
-/**
- * Конвертеры для Room для работы с Enum и другими типами
- */
-class MessageStatusConverter {
- @TypeConverter
- fun fromMessageStatus(status: MessageStatus): String = status.name
-
- @TypeConverter
- fun toMessageStatus(value: String): MessageStatus = runCatching {
- MessageStatus.valueOf(value)
- }.getOrDefault(MessageStatus.UNKNOWN)
-}
diff --git a/client-mobile/chats/data/local/database/UserProfileEntity.kt b/client-mobile/chats/data/local/database/UserProfileEntity.kt
deleted file mode 100644
index 74ac13b..0000000
--- a/client-mobile/chats/data/local/database/UserProfileEntity.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package chats.data.local.database
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-/**
- * Entity для хранения профиля пользователя в локальной БД Room
- */
-@Entity(tableName = "user_profile")
-data class UserProfileEntity(
- @PrimaryKey
- val userId: String,
-
- val username: String,
-
- val displayName: String,
-
- val avatarUrl: String? = null,
-
- val bio: String? = null,
-
- val isOnline: Boolean = false,
-
- val lastSeenMillis: Long = 0,
-
- val updatedAtMillis: Long = System.currentTimeMillis()
-)
diff --git a/client-mobile/chats/data/local/mappers/ChatMappers.kt b/client-mobile/chats/data/local/mappers/ChatMappers.kt
deleted file mode 100644
index ca2bb34..0000000
--- a/client-mobile/chats/data/local/mappers/ChatMappers.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package chats.data.local.mappers
-
-import chats.data.local.database.ChatEntity
-import chats.domain.model.Chat
-import com.google.gson.Gson
-
-private val gson = Gson()
-
-/**
- * Преобразует Domain Chat в Entity
- */
-fun Chat.toEntity(): ChatEntity {
- val timestamp = this.lastMessage?.createdAt?.let { parseTimestamp(it) }
- return ChatEntity(
- localId = this.id.takeIf { it.isNotBlank() } ?: java.util.UUID.randomUUID().toString(),
- remoteId = this.id,
- type = this.type,
- name = this.name,
- avatar = this.avatar,
- unreadCount = this.unreadCount,
- lastMessageText = this.lastMessage?.content,
- lastMessageTimestamp = timestamp,
- lastMessageJson = this.lastMessage?.let { gson.toJson(it) }
- )
-}
-
-/**
- * Преобразует Entity в Domain Chat
- */
-fun ChatEntity.toDomain(): Chat {
- val lastMessage = this.lastMessageJson?.let { json ->
- try {
- gson.fromJson(json, chats.domain.model.Message::class.java)
- } catch (e: Exception) {
- null
- }
- }
- return Chat(
- id = this.remoteId ?: this.localId,
- type = this.type,
- name = this.name,
- avatar = this.avatar,
- unreadCount = this.unreadCount,
- lastMessage = lastMessage
- )
-}
-
-/**
- * Парсит ISO-8601 timestamp в Long (millis)
- */
-private fun parseTimestamp(isoTimestamp: String): Long? {
- return try {
- java.time.ZonedDateTime.parse(isoTimestamp).toInstant().toEpochMilli()
- } catch (e: Exception) {
- null
- }
-}
diff --git a/client-mobile/chats/data/local/mappers/MessageMappers.kt b/client-mobile/chats/data/local/mappers/MessageMappers.kt
deleted file mode 100644
index 328c954..0000000
--- a/client-mobile/chats/data/local/mappers/MessageMappers.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package chats.data.local.mappers
-
-import chats.data.local.database.MessageEntity
-import chats.domain.model.Message
-import chats.domain.model.MessageStatus
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
-
-private val gson = Gson()
-
-/**
- * Преобразует Domain Message в Entity для сохранения в Room
- */
-fun Message.toEntity(status: MessageStatus = MessageStatus.SENT): MessageEntity {
- return MessageEntity(
- localId = this.id.takeIf { it.isNotBlank() } ?: java.util.UUID.randomUUID().toString(),
- serverId = if (status == MessageStatus.SENT || status == MessageStatus.DELIVERED || status == MessageStatus.READ) this.id else null,
- idempotencyKey = java.util.UUID.randomUUID().toString(),
- chatId = this.chatId,
- senderId = this.senderId,
- senderName = this.senderName,
- senderAvatar = this.senderAvatar,
- content = this.content,
- sequenceId = this.sequenceId.toLong(),
- createdAt = this.createdAt,
- mediaType = this.mediaType.name,
- mediaJson = gson.toJson(this.media),
- reactionsJson = gson.toJson(this.reactions),
- status = status,
- isPinned = this.isPinned,
- isForwarded = this.isForwarded,
- forwardedFromName = this.forwardedFromName,
- replyToServerId = this.replyTo?.id,
- createdAtMillis = parseTimestamp(this.createdAt),
- updatedAtMillis = System.currentTimeMillis()
- )
-}
-
-/**
- * Преобразует Entity в Domain Message для отображения в UI
- */
-fun MessageEntity.toDomain(): Message {
- val mediaListType = object : TypeToken>() {}.type
- val reactionsMapType = object : TypeToken