diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fe47d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/.kotlin +/app/build +/app/release diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..5a9c602 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,101 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + kotlin("plugin.serialization") version "1.9.23" +} + +android { + splits { + abi { + isEnable = true + reset() + include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") + isUniversalApk = true + } + } + + namespace = "com.sffteam.voidclient" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "com.sffteam.voidclient" + minSdk = 28 + targetSdk = 36 + versionCode = 1 + versionName = "a2.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // isShrinkResources = true + // isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + viewBinding = true + } + +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compiler) + implementation(libs.androidx.datastore.core) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.material3) + implementation(libs.androidx.ui) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.compose.adaptive) + implementation(libs.androidx.compose.material3.window.size.class1) + implementation(libs.androidx.compose.ui.text) + implementation(libs.androidx.compose.animation) + implementation(libs.androidx.compose.animation.core) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.foundation) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.media3.exoplayer) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) + implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.datastore.preferences) + implementation(libs.guava) + implementation(libs.coil.compose) + implementation(libs.coil.network.okhttp) + implementation("org.lz4:lz4-java:1.8.0") + implementation(libs.kotlinx.datetime) + implementation("io.ktor:ktor-client-cio:3.3.3") + implementation("io.ktor:ktor-client-core:3.3.3") + implementation("io.ktor:ktor-network:3.3.3") + implementation("io.ktor:ktor-network-tls:3.3.3") + implementation("org.msgpack:jackson-dataformat-msgpack:0.9.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") + implementation(libs.autolinktext) + implementation("io.github.g00fy2.quickie:quickie-bundled:1.11.0") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/sffteam/voidclient/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/sffteam/voidclient/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..2e6d8cb --- /dev/null +++ b/app/src/androidTest/java/com/sffteam/voidclient/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.sffteam.openmax + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.sffteam.openmax", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c773189 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..d11593a Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/sffteam/voidclient/AccountManager.kt b/app/src/main/java/com/sffteam/voidclient/AccountManager.kt new file mode 100644 index 0000000..1a3c880 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/AccountManager.kt @@ -0,0 +1,124 @@ +package com.sffteam.voidclient + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlin.collections.plus + +data class Session( + val client: String = "", + val location: String = "", + val current: Boolean = false, + val time: Long = 0L, + val info: String = "" +) + +data class Settings( + val safeMode : Boolean = false, + val searchByPhone : String = "", + val incomingCall : String = "", + val chatsInvite : String = "", + val contentLevelAccess : Boolean = false, + val hidden : Boolean = false +) + +object AccountManager { + private val _sessionsList = MutableStateFlow>(emptyList()) + var sessionsList = _sessionsList.asStateFlow() + + private val _settings = MutableStateFlow(Settings()) + var settings = _settings.asStateFlow() + + var logined: Boolean = false + var accountID: Long = 0L + var token: String = "" + var phone: String = "" + + fun processSettings(settings : JsonObject) { + try { + val safeMode = settings["SAFE_MODE"]?.jsonPrimitive?.boolean + val searchByPhone = settings["SEARCH_BY_PHONE"]?.jsonPrimitive?.content + val incomingCall = settings["INCOMING_CALL"]?.jsonPrimitive?.content + val chatsInvite = settings["CHATS_INVITE"]?.jsonPrimitive?.content + val contentLevelAccess = settings["CONTENT_LEVEL_ACCESS"]?.jsonPrimitive?.boolean + val hidden = settings["HIDDEN"]?.jsonPrimitive?.boolean + + val newSettings = Settings(safeMode!!, searchByPhone!!, incomingCall!!, chatsInvite!!, contentLevelAccess!!, hidden!!) + _settings.update { + newSettings + } + } catch (e : Exception) { + println(e) + } + + println("New settings! ${_settings.value}") + } + fun processSession(sessions: JsonArray) { + for (i in sessions) { + try { + var client: String = "" + var location: String = "" + var current: Boolean = false + var time: Long = 0L + var info: String = "" + + try { + client = i.jsonObject["client"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + println("0msg") + } + + try { + location = + i.jsonObject["location"]!!.jsonPrimitive.content + } catch (e: Exception) { + println("1msg") + println(e) + } + + try { + if (i.jsonObject.containsKey("current")) { + current = i.jsonObject["current"]!!.jsonPrimitive.boolean + } + } catch (e: Exception) { + println("5msg") + println(e) + } + + try { + time = + i.jsonObject["time"]!!.jsonPrimitive.long + } catch (e: Exception) { + println("1msg") + println(e) + } + + try { + info = + i.jsonObject["info"]!!.jsonPrimitive.content + } catch (e: Exception) { + println("1msg") + println(e) + } + + val currentList = listOf( + Session(client, location, current, time, info) + ) + + _sessionsList.update { + it.toList() + currentList + } + + } catch (e: Exception) { + println(e) + } + } + } +} diff --git a/app/src/main/java/com/sffteam/voidclient/ChatActivity.kt b/app/src/main/java/com/sffteam/voidclient/ChatActivity.kt new file mode 100644 index 0000000..c3d1993 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ChatActivity.kt @@ -0,0 +1,2434 @@ +package com.sffteam.voidclient + +import android.app.DownloadManager +import android.annotation.SuppressLint +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.provider.OpenableColumns +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.automirrored.filled.Reply +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Bookmark +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone +import kotlinx.datetime.number +import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import sh.calvin.autolinktext.rememberAutoLinkText +import java.time.Duration +import java.util.Date +import java.util.Locale.getDefault +import kotlin.time.ExperimentalTime +import kotlin.time.Instant.Companion.fromEpochMilliseconds +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking +import kotlin.collections.listOf +import kotlin.collections.set +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.filled.AudioFile +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Download +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.media3.common.util.UnstableApi +import kotlin.math.roundToInt +import androidx.core.net.toUri + +class ChatActivity : ComponentActivity() { + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnrememberedMutableState") + @OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class, ExperimentalTime::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + var chatID: Long = intent.getLongExtra("chatID", 0L) + + if (chatID == AccountManager.accountID) { + chatID = 0L + } + + val messageTime: Long = if (ChatManager.chatsList.value[chatID]?.messages?.isNotEmpty() == true) { + ChatManager.chatsList.value[chatID]!!.messages.toList().last().second.sendTime + } else { + 0L + } + + // TODO : fix this code + if (ChatManager.chatsList.value.isNotEmpty() + && ChatManager.chatsList.value[chatID]?.messages?.isNotEmpty() == true + && ChatManager.chatsList.value[chatID]?.messages?.size!! < 30 + && ChatManager.chatsList.value[chatID]?.needGetMessages == true) { + val packet = SocketManager.packPacket( + OPCode.CHAT_MESSAGES.opcode, JsonObject( + mapOf( + "chatId" to JsonPrimitive(chatID), + "from" to JsonPrimitive(messageTime), + "forward" to JsonPrimitive(0), + "backward" to JsonPrimitive(30), + "getMessages" to JsonPrimitive(true) + ) + ) + ) + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet) + if (packet.payload is JsonObject) ChatManager.processMessages( + packet.payload["messages"]!!.jsonArray, chatID + ) + }) + } + } + + setContent { + AppTheme() { + val chats by ChatManager.chatsList.collectAsState() + + val currentChat = chats[chatID] + val type: String = currentChat?.type.toString() + + println("curren chat ${currentChat}") + println("chats ${chats}") + + val chatTitle: String = if (type == "DIALOG") { + if (chatID == 0L) { + "Избранное" + } else { + intent.getStringExtra("chatTitle")!! + } + } else { + currentChat?.title.toString() + } + + val chatUrl: String = if (type == "DIALOG" && chatID != 0L) { + intent.getStringExtra("chatIcon").toString() + } else { + currentChat?.avatarUrl.toString() + } + + val sortedChats = chats[chatID]?.messages?.toList()?.toList() + ?.sortedByDescending { (_, value) -> value.sendTime } + val listState = rememberLazyListState() + + val coroutineScope = rememberCoroutineScope() + + var showBottomSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() + + var removeforall by remember { mutableStateOf(false) } + + var selectedMSGEdit by remember { mutableLongStateOf(0L) } + var selectedMSGReply by remember { mutableLongStateOf(0L) } + var selectedMSGID by remember { mutableLongStateOf(0L) } + + var showPopup by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } + val isUserScrolling by remember { + derivedStateOf { + listState.isScrollInProgress + } + } + val context = LocalContext.current + val clipboardManager = LocalClipboard.current + + Scaffold( + modifier = Modifier + .fillMaxSize() + .imePadding(), + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceContainer, + ), + title = { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + if (type == "DIALOG") { + val intent = + Intent(context, ProfileViewActivity::class.java) + + var secondUser = 0L + + if (chatID == 0L) { + secondUser = AccountManager.accountID + } else { + for (i in chats[chatID]?.users?.toList()!!) { + if (i.first != AccountManager.accountID) { + secondUser = i.first + } + } + } + + intent.putExtra("userId", secondUser) + + context.startActivity(intent) + } else { + val intent = + Intent(context, ChatViewActivity::class.java) + + intent.putExtra("chatId", chatID) + + context.startActivity(intent) + } + } + .fillMaxWidth() + ) { + IconButton( + onClick = { finish() }, + ) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + contentDescription = "Вернуться в меню" + ) + } + + if (chatUrl.isNotEmpty()) { + AsyncImage( + model = chatUrl, + contentDescription = "ChatIcon", + modifier = Modifier + .width(50.dp) + .height(50.dp) + .clip(CircleShape), + contentScale = ContentScale.FillBounds + ) + } else if (chatID == 0L) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(50.dp) + .height(50.dp) + .clip(CircleShape) + .background(colorScheme.primaryContainer) + ) { + Icon( + Icons.Filled.Bookmark, + contentDescription = "Bookmark", + modifier = Modifier + .size(25.dp) + .align(Alignment.Center) + ) + } + } else { + val initial = chatTitle.split(" ") + .mapNotNull { it.firstOrNull()?.toChar() }.take(2) + .joinToString("").uppercase(getDefault()) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(50.dp) + .height(50.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( // Create a vertical gradient + colors = listOf( + Utils.getColorForAvatar( + chatTitle + ).first, + Utils.getColorForAvatar(chatTitle).second + ) // Define the colors for the gradient + ) + ) + ) { + Text( + text = initial, + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 25.sp, + ) + } + } + Column( + verticalArrangement = Arrangement.Center + ) { + Text( + text = chatTitle, + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + var userDesc: String + + when (type) { + "CHAT" -> { + val sizeString = + chats[chatID]?.users?.size.toString() + val sizeStringLast = sizeString.last().code + userDesc = when (sizeStringLast) { + 1 -> { + "Тут только вы" + } + + else -> { + if (sizeStringLast == 2 || sizeStringLast == 3 || sizeStringLast == 4) { + "$sizeString участника" + } else if (sizeStringLast == 1 && sizeString != "11") { + "$sizeString участник" + } else { + "$sizeString участников" + } + } + } + + } + + "CHANNEL" -> { + userDesc = if (chats[chatID]?.usersCount.toString() + .last().code == 2 || chats[chatID]?.usersCount.toString() + .last().code == 3 || chats[chatID]?.usersCount.toString() + .last().code == 4 + ) { + println( + "code ${ + chats[chatID]?.usersCount.toString() + .last().code + }" + ) + chats[chatID]?.usersCount?.toString() + " подписчика" + } else if (chats[chatID]?.usersCount.toString() + .last().code == 1 && chats[chatID]?.usersCount.toString() != "11" + ) { + chats[chatID]?.usersCount?.toString() + " подписчик" + } else { + chats[chatID]?.usersCount?.toString() + " подписчиков" + } + } + + else -> { + userDesc = if (chatID == 0L) { + "" + } else { + "Был(а) недавно" + } + } + } + + if (userDesc.isNotEmpty()) { + Text( + text = userDesc, + fontSize = 16.sp, + modifier = Modifier.alpha(0.85f) + ) + } + } + } + }, + + ) + }, + bottomBar = { + if (type == "CHANNEL") { + DrawBottomChannel(chatID) + } else { + if (type == "DIALOG") { + var secondUser = 0L + + for (i in chats[chatID]?.users?.toList()!!) { + if (i.first != AccountManager.accountID) { + secondUser = i.first + } + } + if (secondUser != 543835L) { + DrawBottomDialog( + chatID, + selectedMSGReply, + onValChange = { newVal -> selectedMSGReply = newVal }, + type, + onEditChange = { newVal -> selectedMSGEdit = newVal }, + selectedMessageEdit = selectedMSGEdit + ) + } + } else { + DrawBottomDialog( + chatID, + selectedMSGReply, + onValChange = { newVal -> selectedMSGReply = newVal }, + type, + onEditChange = { newVal -> selectedMSGEdit = newVal }, + selectedMessageEdit = selectedMSGEdit + ) + } + } + }, + ) { + LaunchedEffect(isUserScrolling) { + snapshotFlow { listState.layoutInfo.visibleItemsInfo }.collect { visibleItems -> + if (chats[chatID]?.messages?.entries?.isNotEmpty() == true && visibleItems.isNotEmpty()) { + + val listSorted = chats[chatID]?.messages?.entries?.toList() + ?.sortedByDescending { (_, value) -> value.sendTime } + + println("size ${listSorted?.size}") + println("visItems ${visibleItems[0].index}") + println("visItems ${visibleItems.last().index}") + + if (visibleItems.last().index >= listSorted!!.size - 5 && chats[chatID]?.needGetMessages == true && isUserScrolling) { + print("cool: ") + println(listSorted) + val packet = SocketManager.packPacket( + OPCode.CHAT_MESSAGES.opcode, JsonObject( + mapOf( + "chatId" to JsonPrimitive(chatID), + "from" to JsonPrimitive( + listSorted.last().value?.sendTime + ), + "forward" to JsonPrimitive(0), + "backward" to JsonPrimitive(30), + "getMessages" to JsonPrimitive(true) + ) + ) + ) + try { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + ChatManager.processMessages( + packet.payload["messages"]!!.jsonArray, chatID + ) + } + }) + } catch (e : Exception) { + println(e) + } + + } + } + } + } + if (showPopup) { + AlertDialog(title = { + Text(text = "Удалить сообщение") + }, text = { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(0.dp) + ) { + Checkbox( + checked = removeforall, + onCheckedChange = { isChecked -> + removeforall = isChecked + }, + modifier = Modifier.padding(0.dp), + ) + Text( + text = "Удалить у всех", + fontSize = 20.sp, + modifier = Modifier.padding(0.dp) + ) + } + Text( + text = "Вы точно хотите удалить сообщение?", + fontSize = 20.sp, + modifier = Modifier.padding(start = 10.dp) + ) + + } + }, onDismissRequest = { + showPopup = false + removeforall = false + }, confirmButton = { + TextButton( + onClick = { + val packet = SocketManager.packPacket( + OPCode.DELETE_MESSAGE.opcode, JsonObject( + mapOf( + "messageIds" to JsonArray( + listOf( + JsonPrimitive(selectedMSGID) + ) + ), + "chatId" to JsonPrimitive(chatID), + "forMe" to JsonPrimitive(!removeforall), + "itemType" to JsonPrimitive("REGULAR"), + ) + ) + ) + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + if (packet.payload is JsonObject) { + println(packet) + val packetID = + packet.payload["chatId"]?.jsonPrimitive?.long + + for (i in chats[packetID]?.messages!!) { + if (i.key.toLong() == selectedMSGID) { + println(i.key) + println(selectedMSGID) + ChatManager.removeMessage( + chatID, selectedMSGID.toString() + ) + } + } + } + }) + } + + showBottomSheet = false + showPopup = false + removeforall = false + }) { + Text("Удалить", fontSize = 20.sp) + } + }, dismissButton = { + TextButton( + onClick = { + showPopup = false + removeforall = false + }) { + Text("Отмена", fontSize = 20.sp) + } + }) + } + + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + selectedMSGID = 0L + }, sheetState = sheetState + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + selectedMSGReply = selectedMSGID + selectedMSGEdit = 0L + showBottomSheet = false + }) { + Icon( + Icons.AutoMirrored.Filled.Reply, + contentDescription = "reply on message", + modifier = Modifier + .padding(end = 10.dp) + .size(20.dp) + .align(Alignment.CenterVertically) + ) + + Text( + text = "Ответить", + fontSize = 25.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + coroutineScope.launch { + clipboardManager.setClipEntry( + ClipEntry( + ClipData.newPlainText( + chats[chatID]?.messages[selectedMSGID.toString()]?.message.toString(), + chats[chatID]?.messages[selectedMSGID.toString()]?.message.toString() + ) + ) + ) + } + }) { + Icon( + Icons.Filled.ContentCopy, + contentDescription = "edit message", + modifier = Modifier + .padding(end = 10.dp) + .size(20.dp) + .align(Alignment.CenterVertically) + ) + + Text( + text = "Скопировать сообщение", + fontSize = 25.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + + if (chats[chatID]?.messages[selectedMSGID.toString()]?.senderID == AccountManager.accountID) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + selectedMSGEdit = selectedMSGID + showBottomSheet = false + selectedMSGReply = 0L + }) { + Icon( + Icons.Filled.Edit, + contentDescription = "edit message", + modifier = Modifier + .padding(end = 10.dp) + .size(20.dp) + .align(Alignment.CenterVertically) + ) + + Text( + text = "Редактировать", + fontSize = 25.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + + Row( + modifier = Modifier + .clickable { + showPopup = true + } + .fillMaxWidth()) { + Icon( + Icons.Filled.Delete, + contentDescription = "delete message", + modifier = Modifier + .padding(end = 10.dp) + .size(20.dp) + .align(Alignment.CenterVertically), + tint = Color.Red + ) + + Text( + text = "Удалить", + fontSize = 25.sp, + color = Color.Red, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + } + } + } + } + println("jopadsdd") + + if (chats[chatID]?.messages?.isNotEmpty() == true) { + LazyColumn( + modifier = Modifier + .padding(it) + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(6.dp), + state = listState, + reverseLayout = true + ) { + println("qf") + + itemsIndexed( + sortedChats ?: emptyList(), key = { index, message -> + message.first + }) { index, message -> + println("qff chatId ${chatID}") + + val horizontal: Alignment.Horizontal = + if (message.second.senderID == AccountManager.accountID && (message.second.attaches is JsonArray && !(message.second.attaches?.jsonArray?.isNotEmpty() == true && message.second.attaches?.jsonArray?.last()?.jsonObject?.contains( + "event" + ) == true)) + ) { + Alignment.End + } else if (message.second.attaches is JsonArray && message.second.attaches?.jsonArray?.isNotEmpty() == true && message.second.attaches?.jsonArray?.last()?.jsonObject?.contains( + "event" + ) == true + ) { + Alignment.CenterHorizontally + } else { + Alignment.Start + } + + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + val prevMsg = + if (index != sortedChats?.size?.minus(1)) sortedChats?.get(index + 1)?.second else Message() + val duration = Duration.ofSeconds( + message.second.sendTime / 1000 - (prevMsg?.sendTime?.div( + 1000 + ) ?: 0) + ) + + val instantLast = fromEpochMilliseconds(message.second.sendTime) + val instantPrev = fromEpochMilliseconds(prevMsg?.sendTime!!) + + val localDateTimeLastPrev = + instantPrev.toLocalDateTime(TimeZone.currentSystemDefault()) + val localDateTimeLast = + instantLast.toLocalDateTime(TimeZone.currentSystemDefault()) + + if (localDateTimeLastPrev.date != localDateTimeLast.date) { + val currentTime = Date().time + + val durCool = + Duration.ofSeconds(currentTime / 1000 - message.second.sendTime / 1000) + val instantLast = fromEpochMilliseconds(message.second.sendTime) + + val text = if (durCool.toHours() < 24) { + "Сегодня" + } else if (durCool.toHours() in 24..<48) { + "Вчера" + } else { + "${localDateTimeLast.day}.${localDateTimeLast.month.number}.${localDateTimeLast.year}" + } + + Box( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .background( + colorScheme.secondaryContainer.copy(alpha = 0.6f), + shape = RoundedCornerShape(14.dp) + ), + ) { + Text( + text, modifier = Modifier.padding( + bottom = 3.dp, start = 6.dp, end = 6.dp + ), color = colorScheme.onSecondaryContainer + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + indication = null, + interactionSource = interactionSource + ) { + if (message.second.attaches is JsonArray && + (message.second.attaches!!.jsonArray.isEmpty() || !message.second.attaches!!.jsonArray.last().jsonObject.containsKey( + "event" + )) + ) { + showBottomSheet = true + selectedMSGID = message.first.toLong() + } + }, + horizontalArrangement = Arrangement.spacedBy( + 16.dp, horizontal + ), + ) { + DrawMessage( + message.second, + type, + if (index != sortedChats?.size?.minus(1)) sortedChats?.get( + index + 1 + )?.second ?: Message() else Message(), + if (index > 0) sortedChats?.get(index - 1)?.second + ?: Message() else Message(), + chatID, + message.first.toLong() + ) + } + } + } + } + } + + val isBottom by remember { + derivedStateOf { + val visibleItems = listState.layoutInfo.visibleItemsInfo + + visibleItems.isNotEmpty() && visibleItems.first().index > 10 + } + } + val density = LocalDensity.current + AnimatedVisibility(visible = isBottom, enter = slideInVertically { + with(density) { 50.dp.roundToPx() } + }, exit = slideOutVertically { + with(density) { +70.dp.roundToPx() } + }) { + println("butotn") + Box( + contentAlignment = Alignment.BottomEnd, + modifier = Modifier + .background(Color.Transparent) + .fillMaxSize() + .padding(end = 8.dp, bottom = 8.dp) + ) { + IconButton( + onClick = { + coroutineScope.launch { + listState.animateScrollToItem(0, 0) + } + }, + modifier = Modifier + .padding(it) + .background(colorScheme.surfaceContainer, CircleShape) + .size(50.dp) + ) { + Icon( + Icons.Filled.ArrowDropDown, + contentDescription = "scrollToBottom" + ) + } + } + } + val isAtBottom by remember { + derivedStateOf { + val visibleItems = listState.layoutInfo.visibleItemsInfo + + visibleItems.isNotEmpty() && visibleItems.first().index < 5 + } + } + + LaunchedEffect(chats) { + println("testttt") + if (isAtBottom) { + listState.scrollToItem( + index = 0, scrollOffset = 0 + ) + } + } + val reply = selectedMSGReply != 0L + val edit = selectedMSGEdit != 0L + + // TODO: rewrite edit and reply message box + Box( + modifier = Modifier + .background(Color.Transparent) + .fillMaxSize() + .padding(it), + contentAlignment = Alignment.BottomEnd + ) { + // TODO: fix null user on exit animation + AnimatedVisibility(visible = edit, enter = slideInVertically { + with(density) { 50.dp.roundToPx() } + }, exit = slideOutVertically { + with(density) { +70.dp.roundToPx() } + }) { + Box( + modifier = Modifier + .background(colorScheme.surfaceContainer) + .fillMaxWidth() + .padding(start = 10.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + Icons.Filled.Edit, + contentDescription = "edit icon", + modifier = Modifier + .size(25.dp) + .padding(end = 8.dp) + ) + + val users by UserManager.usersList.collectAsState() + val selectedMsg = + chats[chatID]?.messages[selectedMSGEdit.toString()] + + Column() { + val user = users[selectedMsg?.senderID] + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + + val userName = if (user?.lastName?.isNotEmpty() == true) { + user.firstName + " " + user.lastName + } else { + user?.firstName + } + + Text("Редактирование", color = colorScheme.primary) + + val annotatedString = buildAnnotatedString { + if (selectedMsg?.attaches?.jsonArray?.isNotEmpty() == true) { + appendInlineContent(id = "lastImg") + append(" ") + } + + if (selectedMsg?.message?.isNotEmpty() == true) { + append(selectedMsg.message) + } else { + append("Фотография") + } + } + + val inlineContentMap = + mutableMapOf() + + val placeholder = Placeholder( + width = 25.sp, + height = 25.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter + ) + + inlineContentMap["lastImg"] = + InlineTextContent(placeholder) { _ -> + AsyncImage( + model = selectedMsg?.attaches?.jsonArray!!.last().jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .size(25.dp) + .clip(RoundedCornerShape(2.dp)), + contentScale = ContentScale.Crop + ) + } + + Text( + annotatedString, + inlineContent = inlineContentMap, + modifier = Modifier.sizeIn(maxWidth = screenWidth * 0.85f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + } + + Spacer(modifier = Modifier.weight(0.5f)) + + IconButton( + onClick = { + selectedMSGEdit = 0L + }, + ) { + Icon(Icons.Filled.Close, contentDescription = "close") + } + } + } + } + } + + Box( + modifier = Modifier + .background(Color.Transparent) + .fillMaxSize() + .padding(it), + contentAlignment = Alignment.BottomEnd + ) { + // TODO: fix null user on exit animation + AnimatedVisibility(visible = reply, enter = slideInVertically { + with(density) { 50.dp.roundToPx() } + }, exit = slideOutVertically { + with(density) { +70.dp.roundToPx() } + }) { + Box( + modifier = Modifier + .background(colorScheme.surfaceContainer) + .fillMaxWidth() + .padding(start = 10.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + Icons.AutoMirrored.Filled.Reply, + contentDescription = "reply icon", + modifier = Modifier + .size(25.dp) + .padding(end = 8.dp) + ) + + val users by UserManager.usersList.collectAsState() + val selectedMsg = + chats[chatID]?.messages[selectedMSGReply.toString()] + + Column() { + val user = users[selectedMsg?.senderID] + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + + val userName = if (user?.lastName?.isNotEmpty() == true) { + user.firstName + " " + user.lastName + } else { + user?.firstName + } + + Text("В ответ $userName", color = colorScheme.primary) + + val annotatedString = buildAnnotatedString { + if (selectedMsg?.attaches?.jsonArray?.isNotEmpty() == true && + selectedMsg?.attaches?.jsonArray?.last()?.jsonObject["_type"]?.jsonPrimitive?.content == "PHOTO") { + appendInlineContent(id = "lastImg") + append(" ") + } + + if (selectedMsg?.message?.isNotEmpty() == true) { + append(selectedMsg.message) + } else { + append("Фотография") + } + } + + val inlineContentMap = + mutableMapOf() + + val placeholder = Placeholder( + width = 25.sp, + height = 25.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter + ) + + if (selectedMsg?.attaches?.jsonArray?.isNotEmpty() == true + && selectedMsg?.attaches?.jsonArray?.last()?.jsonObject["_type"]?.jsonPrimitive?.content == "PHOTO") { + inlineContentMap["lastImg"] = + InlineTextContent(placeholder) { _ -> + AsyncImage( + model = selectedMsg?.attaches?.jsonArray!!.last().jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .size(25.dp) + .clip(RoundedCornerShape(2.dp)), + contentScale = ContentScale.Crop + ) + } + } + + Text( + annotatedString, + inlineContent = inlineContentMap, + modifier = Modifier.sizeIn(maxWidth = screenWidth * 0.85f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + } + + Spacer(modifier = Modifier.weight(0.5f)) + + IconButton( + onClick = { + selectedMSGReply = 0L + }, + ) { + Icon(Icons.Filled.Close, contentDescription = "close") + } + } + } + } + } + } + } + } + } +} + +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +@SuppressLint("ServiceCast", "UseKtx") +@OptIn(ExperimentalTime::class, DelicateCoroutinesApi::class) +@Composable +fun DrawMessage( + message: Message, chatType: String, previousMessage: Message, nextMessage: Message, chatId : Long, messageId : Long +) { + println("msg ${message}") + val users by UserManager.usersList.collectAsState() + + var username = users[message.senderID]?.firstName + + val coroutineScope = rememberCoroutineScope() + val contextWidth by remember { mutableFloatStateOf(0f) } + val offset = remember { + Animatable(initialValue = 0f) + } + + val context = LocalContext.current + + if (users[message.senderID]?.lastName?.isNotEmpty() == true) { + username += " " + users[message.senderID]?.lastName + } + + if (message.attaches is JsonArray && + !(message.attaches.jsonArray.isNotEmpty() && message.attaches.jsonArray.last().jsonObject.contains( + "event" + )) + ) { + Row( + modifier = Modifier.padding( + start = if (chatType != "CHAT" || (message.senderID != AccountManager.accountID && nextMessage.senderID != message.senderID)) 6.dp else 55.dp, + end = 6.dp + ), horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start) + ) { + if (chatType == "CHAT" && message.senderID != AccountManager.accountID && nextMessage.senderID != message.senderID) { + if (!users.containsKey(message.senderID)) { + val packet = SocketManager.packPacket( + OPCode.CONTACTS_INFO.opcode, JsonObject( + mapOf( + "contactIds" to JsonArray( + listOf( + Json.encodeToJsonElement( + Long.serializer(), message.senderID + ) + ) + ), + ) + ) + ) + + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + GlobalScope.launch { + UserManager.processUsers(packet.payload["contacts"]!!.jsonArray) + } + } + } + ) + } + } + + if (users[message.senderID]?.avatarUrl?.isNotEmpty() == true) { + AsyncImage( + model = users[message.senderID]?.avatarUrl, + contentDescription = "ChatIcon", + modifier = Modifier + .width(45.dp) + .height(45.dp) + .clip(CircleShape) + .align(Alignment.Bottom) + .clickable { + val intent = Intent(context, ProfileViewActivity::class.java) + + intent.putExtra("userId", message.senderID) + + context.startActivity(intent) + }, + contentScale = ContentScale.Crop, + ) + } else { + val initial = username?.split(" ")?.mapNotNull { it.firstOrNull() }?.take(2) + ?.joinToString("")?.uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(45.dp) + .height(45.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(username.toString()).first, + Utils.getColorForAvatar(username.toString()).second + ) + ) + ) + .align(Alignment.Bottom) + .clickable { + val intent = Intent(context, ProfileViewActivity::class.java) + + intent.putExtra("userId", message.senderID) + + context.startActivity(intent) + } + ) { + Text( + text = initial.toString(), + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 25.sp + ) + } + } + } + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + + Box( + modifier = Modifier + .sizeIn(minWidth = 100.dp, maxWidth = screenWidth * 0.7f) + .clip(RoundedCornerShape(16.dp)) + .background(color = if (message.senderID == AccountManager.accountID) colorScheme.primaryContainer else colorScheme.secondaryContainer) + .padding(start = 4.dp, end = 4.dp, top = 4.dp) + ) { + Column { + var type = "" + var attach : JsonObject + + if (message.attaches.isNotEmpty()) { + attach = message.attaches.last().jsonObject + + type = attach.jsonObject["_type"]!!.jsonPrimitive.content + } + + if (type != "" && type != "PHOTO") { + attach = message.attaches.last().jsonObject + + when (type) { + "FILE" -> { + val name = attach.jsonObject["name"]?.jsonPrimitive?.content!! + val size = attach.jsonObject["size"]?.jsonPrimitive?.long!! + val id = attach.jsonObject["fileId"]?.jsonPrimitive?.long!! + + val convertedSize = Utils.getSizeFromBytes(size) + val icon = Utils.getIconForFile(name) + + Row(modifier = Modifier + .padding(bottom = 15.dp) + .clickable { + val packet = SocketManager.packPacket( + OPCode.GET_FILE.opcode, JsonObject( + mapOf( + "fileId" to JsonPrimitive(id), + "chatId" to JsonPrimitive(chatId), + "messageId" to JsonPrimitive(messageId) + ) + ) + ); + var url = "" + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + url = + packet.payload["url"]?.jsonPrimitive?.content.toString() + + val downloadManager = + context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + + val request = + DownloadManager.Request(url.toUri()) + request.setTitle(name) + request.setNotificationVisibility( + DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED + ) + request.setDestinationInExternalPublicDir( + Environment.DIRECTORY_DOWNLOADS, + name + ) + downloadManager.enqueue(request) + } + }); + } + }, + horizontalArrangement = Arrangement.spacedBy(4.dp)) { + + Icon( + icon, + "", + modifier = Modifier.size(45.dp) + ) + + Column(modifier = Modifier.align(Alignment.CenterVertically)) { + Text( + text = name, + fontSize = 18.sp, + ) + + Text( + text = convertedSize, + fontSize = 18.sp + ) + } + + Icon( + Icons.Filled.Download, + "", + modifier = Modifier + .size(20.dp) + .align(Alignment.CenterVertically) + ) + } + } + } + } else { + if (chatType == "CHAT" && message.senderID != AccountManager.accountID && ((previousMessage.attaches?.jsonArray?.isNotEmpty() == true + && previousMessage.attaches.jsonArray.last().jsonObject.containsKey( + "event" + )) || previousMessage.senderID != message.senderID) + ) { + Text( + username.toString(), + color = Utils.getColorForNickname(username.toString()), + fontSize = 16.sp, + modifier = Modifier + .padding(start = 2.dp, end = 2.dp) + .clickable { + val intent = + Intent(context, ProfileViewActivity::class.java) + + intent.putExtra("userId", message.senderID) + + context.startActivity(intent) + } + ) + } else { + println("prevmsg $previousMessage") + } + if (message.link.type.isNotEmpty() && message.link.type == "FORWARD") { + if (!users.containsKey(message.link.msgForLink.senderID)) { + val packet = SocketManager.packPacket( + OPCode.CONTACTS_INFO.opcode, JsonObject( + mapOf( + "contactIds" to JsonArray( + listOf( + Json.encodeToJsonElement( + Long.serializer(), + message.link.msgForLink.senderID + ) + ) + ), + ) + ) + ) + + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + GlobalScope.launch { + UserManager.processUsers(packet.payload["contacts"]!!.jsonArray) + } + } + }) + } + } + val fromUserForward = + users[message.link.msgForLink.senderID]?.firstName + " " + users[message.link.msgForLink.senderID]?.lastName + + val annotatedString = buildAnnotatedString { + withStyle(style = SpanStyle(fontSize = 15.sp)) { + append("Переслано от: ") + } + + appendInlineContent(id = "avatar") + append(" ") + withStyle(style = SpanStyle(fontSize = 15.sp)) { + append(fromUserForward) + } + } + val inlineContentMap = mutableMapOf( + + ) + val placeholder = Placeholder( + width = 30.sp, + height = 30.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center + ) + + inlineContentMap["avatar"] = InlineTextContent(placeholder) { _ -> + if (users[message.link.msgForLink.senderID]?.avatarUrl?.isNotEmpty() == true) { + AsyncImage( + model = users[message.link.msgForLink.senderID]?.avatarUrl, + contentDescription = "ChatIcon", + modifier = Modifier + .width(30.dp) + .height(30.dp) + .clip(CircleShape) + .fillMaxSize(), + alignment = Alignment.Center, + contentScale = ContentScale.FillBounds + ) + } else { + val initial = + fromUserForward.split(" ").mapNotNull { it.firstOrNull() } + .take(2).joinToString("").uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(30.dp) + .height(30.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(fromUserForward).first, + Utils.getColorForAvatar(fromUserForward).second + ) + ) + ) + .fillMaxSize() + .padding(start = 2.dp, end = 2.dp) + ) { + Text( + text = initial, + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 10.sp + ) + } + } + } + + Text( + annotatedString, + inlineContent = inlineContentMap, + fontSize = 15.sp, + modifier = Modifier + .padding(start = 2.dp, end = 2.dp) + .heightIn(max = 50.dp), + color = if (message.senderID == AccountManager.accountID) colorScheme.onPrimaryContainer else colorScheme.onSecondaryContainer + ) + if (message.link.msgForLink.attaches is JsonArray) { + DrawImages(message.link.msgForLink.attaches.jsonArray, context, chatId) + } + + Text( + message.link.msgForLink.message, + fontSize = 16.sp, + modifier = Modifier.padding(start = 6.dp, end = 6.dp, bottom = 16.dp), + color = if (message.senderID == AccountManager.accountID) colorScheme.onPrimaryContainer else colorScheme.onSecondaryContainer + ) + } else { + if (message.link.type.isNotEmpty() && message.link.type == "REPLY") { + Box( + modifier = Modifier + .background( + color = if (message.senderID == AccountManager.accountID) colorScheme.inversePrimary.copy( + 0.6f + ) else colorScheme.onSecondary.copy(0.6f), + shape = RoundedCornerShape(8.dp) + ) + .padding(start = 2.dp, end = 2.dp) + .sizeIn(minWidth = 100.dp, maxWidth = screenWidth * 0.7f) + ) { + var userName = users[message.link.msgForLink.senderID]?.firstName + + if (users[message.link.msgForLink.senderID]?.lastName?.isNotEmpty() == true) { + userName += " " + users[message.link.msgForLink.senderID]?.lastName + } + + if (!users.containsKey(message.link.msgForLink.senderID)) { + val packet = SocketManager.packPacket( + OPCode.CONTACTS_INFO.opcode, JsonObject( + mapOf( + "contactIds" to JsonArray( + listOf( + Json.encodeToJsonElement( + Long.serializer(), + message.link.msgForLink.senderID + ) + ) + ), + ) + ) + ) + + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + GlobalScope.launch { + UserManager.processUsers(packet.payload["contacts"]!!.jsonArray) + } + } + }) + } + } + + Column() { + Text( + userName.toString(), + fontSize = 13.sp, + modifier = Modifier.padding(start = 2.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = if (message.senderID == AccountManager.accountID) colorScheme.onPrimaryContainer else colorScheme.onSecondaryContainer + ) + + val annotatedString = buildAnnotatedString { + if (message.link.msgForLink.attaches is JsonArray + && message.link.msgForLink.attaches.jsonArray.isNotEmpty() + && message.link.msgForLink.attaches.jsonArray.last().jsonObject["_type"]!!.jsonPrimitive.content == "PHOTO") { + appendInlineContent(id = "lastImg") + append(" ") + } + + if (message.link.msgForLink.message.isNotEmpty()) { + append(message.link.msgForLink.message) + } else { + append("Фотография") + } + } + + val inlineContentMap = mutableMapOf() + + val placeholder = Placeholder( + width = 25.sp, + height = 25.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter + ) + + inlineContentMap["lastImg"] = + InlineTextContent(placeholder) { _ -> + AsyncImage( + model = message.link.msgForLink.attaches?.jsonArray!!.last().jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .size(25.dp) + .clip(RoundedCornerShape(2.dp)), + contentScale = ContentScale.Crop + ) + } + + Text( + text = annotatedString, + inlineContent = inlineContentMap, + fontSize = 12.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier.padding(start = 2.dp, bottom = 4.dp), + color = if (message.senderID == AccountManager.accountID) colorScheme.onPrimaryContainer else colorScheme.onSecondaryContainer + ) + } + } + } + if (message.attaches!!.jsonArray.isNotEmpty()) { + DrawImages(message.attaches.jsonArray, context, chatId) + } + + Text( + AnnotatedString.rememberAutoLinkText( + message.message + ), autoSize = TextAutoSize.StepBased( + minFontSize = 10.sp, maxFontSize = 16.sp + ), modifier = Modifier.padding(start = 2.dp, end = 6.dp, bottom = 16.dp), + color = if (message.senderID == AccountManager.accountID) colorScheme.onPrimaryContainer else colorScheme.onSecondaryContainer + ) + } + } + } + + val instant = fromEpochMilliseconds(message.sendTime) + val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) + var minutezero: String = "0" + + minutezero = if (localDateTime.minute < 10) { + "0" + localDateTime.minute.toString() + } else { + localDateTime.minute.toString() + } + + val time = "${localDateTime.hour}:${minutezero}" + + Row( + modifier = Modifier + .padding(top = 20.dp, end = 4.dp) + .align(Alignment.BottomEnd), + ) { + if (message.status == "EDITED") { + Icon( + Icons.Filled.Edit, + contentDescription = "add", + modifier = Modifier + .size(15.dp) + .align( + Alignment.CenterVertically + ) + .alpha(0.8f) + .padding(end = 2.dp), + tint = if (message.senderID == AccountManager.accountID) colorScheme.onPrimaryContainer else colorScheme.onSecondaryContainer + ) + } + + Text( + time, modifier = Modifier + .align( + Alignment.CenterVertically + ) + .alpha(0.8f), fontSize = 14.sp, + color = if (message.senderID == AccountManager.accountID) colorScheme.onPrimaryContainer else colorScheme.onSecondaryContainer + ) + } + } + } + } else if (message.attaches is JsonArray) { + val users by UserManager.usersList.collectAsState() + val attach = message.attaches?.jsonArray?.last() + + val type = attach!!.jsonObject["_type"]?.jsonPrimitive?.content + + if (type == "CONTROL") { + val event = attach?.jsonObject["event"]?.jsonPrimitive?.content + var userName = "" + + Box( + modifier = Modifier + .padding(bottom = 3.dp, start = 6.dp, end = 6.dp) + .background( + colorScheme.secondaryContainer.copy(alpha = 0.6f), + shape = RoundedCornerShape(14.dp) + ), + contentAlignment = Alignment.Center + ) { + var joinText = "" + // shit code :roflan_ebalo: + + if (event == "joinByLink") { + UserManager.checkForExisting(attach.jsonObject["userId"]?.jsonPrimitive?.long!!) + + if (message.senderID == AccountManager.accountID) { + joinText += "Вы присоединились к чату" + } else { + userName = + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.firstName.toString() + + if (users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName?.isNotEmpty() == true) { + userName += " " + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName + } + joinText += "$username присоединился(-ась) к чату" + } + } else if (event == "add") { + val peoplesAdded = attach.jsonObject["userIds"]?.jsonArray + + for (i in peoplesAdded!!) { + if (attach.jsonObject["userIds"]?.jsonArray?.isNotEmpty() == true) { + UserManager.checkForExisting(i.jsonPrimitive.long) + } + } + + UserManager.checkForExisting(message.senderID) + + if (message.senderID == AccountManager.accountID) { + joinText += "Вы добавили " + } else { + var whoAdded = users[message.senderID]?.firstName.toString() + + if (users[message.senderID]?.firstName?.isNotEmpty() == true) { + whoAdded += " " + users[message.senderID]?.lastName + } + + joinText += "$whoAdded добавил(-а) " + } + + for (i in peoplesAdded) { + var whomAdded = users[i.jsonPrimitive.long]?.firstName.toString() + + if (users[i.jsonPrimitive.long]?.lastName?.isNotEmpty() == true) { + whomAdded += " " + users[i.jsonPrimitive.long]?.lastName + } + + joinText += whomAdded + if (i != peoplesAdded.last()) { + joinText += ", " + } + } + } else if (event == "leave") { + UserManager.checkForExisting(message.senderID) + + if (message.senderID == AccountManager.accountID) { + joinText += "Вы покинули чат" + } else { + userName = + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.firstName.toString() + + if (users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName?.isNotEmpty() == true) { + userName += " " + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName + } + joinText += "$username покинул(-а) чат" + } + } else if (event == "title") { + var userName = "" + UserManager.checkForExisting(message.senderID) + + if (message.senderID == AccountManager.accountID) { + username = "Вы" + val newTitle = attach.jsonObject["title"]?.jsonPrimitive?.content + + joinText += "$username изменили название чата на «$newTitle»" + } else { + userName = + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.firstName.toString() + + if (users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName?.isNotEmpty() == true) { + userName += " " + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName + } + + val newTitle = attach.jsonObject["title"]?.jsonPrimitive?.content + + joinText += "$username изменил(-а) название чата на «$newTitle»" + } + } else if (event == "icon") { + var userName = "" + + UserManager.checkForExisting(message.senderID) + + if (chatType == "CHANNEL") { + joinText += "Изменено фото канала" + } else { + if (message.senderID == AccountManager.accountID) { + username = "Вы" + val newTitle = attach.jsonObject["title"]?.jsonPrimitive?.content + + joinText += "$username изменили фото чата" + } else { + userName = + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.firstName.toString() + + if (users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName?.isNotEmpty() == true) { + userName += " " + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName + } + + val newTitle = attach.jsonObject["title"]?.jsonPrimitive?.content + + joinText += "$username изменил(-а) фото чата" + } + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(joinText, + color = colorScheme.onSecondaryContainer) + + AsyncImage( + model = attach.jsonObject["url"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .clip(CircleShape) + .size(100.dp) + .clickable { + val intent = Intent(context, ImageViewerActivity::class.java) + + intent.putExtra("isSingleImage", true) + intent.putExtra( + "image", + attach.jsonObject["fullUrl"]!!.jsonPrimitive.content + ) + + context.startActivity(intent) + }, + contentScale = ContentScale.Crop + ) + } + } else if (event == "new") { + var userName = "" + + UserManager.checkForExisting(message.senderID) + + if (chatType != "CHANNEL") { + if (message.senderID == AccountManager.accountID) { + username = "Вы" + + joinText += "$username создали чат" + } else { + userName = + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.firstName.toString() + + if (users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName?.isNotEmpty() == true) { + userName += " " + users[attach.jsonObject["userId"]?.jsonPrimitive?.long]?.lastName + } + + joinText += "$username создал(-а) чат" + } + } else { + joinText += "Канал создан" + } + } else if (event == "remove") { + val peoplesRemoved = attach.jsonObject["userId"]?.jsonPrimitive?.long + + UserManager.checkForExisting(peoplesRemoved!!) + UserManager.checkForExisting(message.senderID) + + var whomAdded = users[peoplesRemoved]?.firstName.toString() + + if (users[peoplesRemoved]?.lastName?.isNotEmpty() == true) { + whomAdded += " " + users[peoplesRemoved]?.lastName + } + if (message.senderID == AccountManager.accountID) { + joinText += "Вы удалили $whomAdded" + } else { + var whoAdded = users[message.senderID]?.firstName.toString() + + if (users[message.senderID]?.firstName?.isNotEmpty() == true) { + whoAdded += " " + users[message.senderID]?.lastName + } + + joinText += "$whoAdded удалил(-а) $whomAdded" + } + } else if (event == "system") { + joinText += attach.jsonObject["message"]?.jsonPrimitive?.content + } + + if (event != "icon") { + Text( + joinText, modifier = Modifier + .align(Alignment.Center) + .padding(start = 3.dp, end = 3.dp), + color = colorScheme.onSecondaryContainer + ) + } + } + } + } +} + +@OptIn(DelicateCoroutinesApi::class) +@Composable +fun DrawBottomDialog( + chatID: Long, + selectedMessage: Long, + onValChange: (Long) -> Unit, + chatType: String, + selectedMessageEdit: Long, + onEditChange: (Long) -> Unit +) { + var message by remember { mutableStateOf("") } + var selectedImages by remember { + mutableStateOf>(emptyList()) + } + + var uploadedImages = remember { + mutableListOf>() + } + + val context = LocalContext.current + Column() { + // Can message be with id == 0? :thinking: + val chats by ChatManager.chatsList.collectAsState() + if (selectedMessageEdit != 0L) { + val selectedEdit = chats[chatID]?.messages[selectedMessageEdit.toString()] + + message = selectedEdit?.message!! + } else { + message = "" + } + + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + items(selectedImages) { image -> + Box(modifier = Modifier.background(colorScheme.primaryContainer, RoundedCornerShape(8.dp))) { + AsyncImage( + model = image, + contentDescription = null, + modifier = Modifier.size(75.dp), + contentScale = ContentScale.Fit + ) + } + } + } + + BottomAppBar { + val multiplePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = 10), + onResult = { uris -> + if (uris.isNotEmpty()) { + for (i in uris) { + selectedImages = uris + + var imageType = "" + var imageName = "" + + val cursor = context.contentResolver.query(i, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + + imageName = it.getString(nameIndex) + imageType = context.contentResolver.getType(i).toString() + } + } + + val packet = SocketManager.packPacket( + OPCode.UPLOAD_IMAGE.opcode, JsonObject( + mapOf( + "count" to JsonPrimitive(1) + ) + ) + ) + val client = HttpClient(CIO) + + runBlocking { + val imageBytes = try { + context.contentResolver.openInputStream(uris.last()) + ?.use { inputStream -> + inputStream.readBytes() + } + } catch (e: Exception) { + null + } + + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + runBlocking { + try { + val response: HttpResponse = + client.post(packet.payload["url"]?.jsonPrimitive?.content.toString()) { + method = HttpMethod.Post + + headers { + append( + HttpHeaders.UserAgent, + "OKMessages/25.12.1 (Android 14; oneplus CPH2465; 382dpi 2300x1023)" + ) + append( + HttpHeaders.ContentType, + "application/octet-stream" + ) + append( + HttpHeaders.ContentDisposition, + "attachment; filename=${imageName}" + ) + append( + "X-Uploading-Mode", "parallel" + ) + append( + "Content-Range", + "bytes 0-${imageBytes!!.size - 1}/${imageBytes.size}" + ) + append( + HttpHeaders.Connection, "keep-alive" + ) + append( + HttpHeaders.AcceptEncoding, "gzip" + ) + } + + setBody(imageBytes) + } + + println(response.request.content) + println("Upload response status: ${response.status}") + val content = + Json.parseToJsonElement(response.bodyAsText()) + + uploadedImages += Pair( + i, content.jsonObject["photos"]!!.jsonObject + ) + + print(content) + } catch (e: Exception) { + e.printStackTrace() + } finally { + client.close() + } + } + } + }) + } + } + } + }) + + IconButton(onClick = { + multiplePhotoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + }) { + Icon(Icons.Filled.Add, contentDescription = "add") + } + + + OutlinedTextField( + value = message, + onValueChange = { newText -> + message = newText + }, + placeholder = { Text("Сообщение") }, + shape = RoundedCornerShape(20.dp), + modifier = Modifier.weight(1f) + ) + + IconButton(onClick = { + if (selectedMessageEdit == 0L) { + if (message.isNotEmpty() || (uploadedImages.isNotEmpty() && uploadedImages.size == selectedImages.size)) { + println(uploadedImages) + println(selectedImages) + var messageObject = mutableMapOf( + "isLive" to JsonPrimitive(false), + "detectShare" to JsonPrimitive(true), + "elements" to JsonArray(emptyList()), + "cid" to JsonPrimitive(System.currentTimeMillis()), + ) + + var secondUser = 1L + + if (message.isNotEmpty()) { + messageObject["text"] = JsonPrimitive(message) + } + + if (uploadedImages.isNotEmpty()) { + var coolJson = mutableListOf() + for (i in uploadedImages) { + println("elm $i") + println( + "secon ${ + i.second.toList().last().second.jsonObject["token"] + }" + ) + coolJson += JsonObject( + mapOf( + "photoToken" to JsonPrimitive( + i.second.toList() + .last().second.jsonObject["token"]!!.jsonPrimitive.content + ), "_type" to JsonPrimitive("PHOTO") + ) + ) + println("cool jsn2 $coolJson") + } + + println("cool jsn $coolJson") + messageObject["attaches"] = JsonArray(coolJson) + } + + if (chatType == "DIALOG") { + for (i in chats[chatID]?.users?.toList()!!) { + if (i.first != AccountManager.accountID) { + secondUser = i.first + break + } + } + } + + if (selectedMessage != 0L) { + messageObject["link"] = JsonObject( + mapOf( + "type" to JsonPrimitive("REPLY"), + "chatId" to JsonPrimitive(chatID), + "messageId" to JsonPrimitive(selectedMessage), + ) + ) + } + + val packetJson = JsonObject( + mapOf( + if (chatType == "CHAT") "chatId" to JsonPrimitive(chatID) else if (chatID == 0L) "userId" to JsonPrimitive( + AccountManager.accountID + ) else "userId" to JsonPrimitive(secondUser), + "message" to JsonObject( + messageObject + ) + ) + ) + + val packet = + SocketManager.packPacket(OPCode.SEND_MESSAGE.opcode, packetJson) + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + println("msg should be added") + if (packet.payload is JsonObject) { + println("msg should be added") + var msgID = "" + var msg = Message("", 0L, 0L, JsonArray(emptyList()), "") + + try { + var status = "" + if (packet.payload.jsonObject["message"]?.jsonObject?.containsKey( + "status" + ) == true + ) { + try { + status = + packet.payload.jsonObject["message"]?.jsonObject["status"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + } + var textForwarded: String = "" + var senderForwarded: Long = 0L + var msgForwardedID: String = "" + var forwardedAttaches: JsonElement? = JsonNull + var forwardedType: String = "" + + if (packet.payload.jsonObject["message"]!!.jsonObject.contains( + "link" + ) + ) { + val messageLinked = + packet.payload.jsonObject["message"]?.jsonObject["link"]?.jsonObject["message"] + + textForwarded = + messageLinked?.jsonObject["text"]?.jsonPrimitive?.content.toString() + senderForwarded = + messageLinked?.jsonObject["sender"]?.jsonPrimitive!!.long + msgForwardedID = + messageLinked.jsonObject["id"]?.jsonPrimitive!!.long.toString() + forwardedType = + packet.payload.jsonObject["message"]?.jsonObject["link"]?.jsonObject["type"]?.jsonPrimitive?.content.toString() + } + + msg = Message( + packet.payload["message"]?.jsonObject["text"]!!.jsonPrimitive.content, + packet.payload["message"]?.jsonObject["time"]!!.jsonPrimitive.long, + packet.payload["message"]?.jsonObject["sender"]!!.jsonPrimitive.long, + packet.payload["message"]?.jsonObject["attaches"]!!.jsonArray, + status, + MessageLink( + type = forwardedType, msgForLink = msgForLink( + textForwarded, + senderID = senderForwarded, + msgID = msgForwardedID + ) + ) + ) + + msgID = + packet.payload["message"]?.jsonObject["id"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + println(msg) + ChatManager.addMessage(msgID, msg, chatID) + + } + }) + onValChange(0L) + + uploadedImages = mutableListOf() + selectedImages = mutableListOf() + } + } + } else { + val selectedEdit = chats[chatID]?.messages[selectedMessageEdit.toString()] + + val packet = SocketManager.packPacket( + OPCode.EDIT_MESSAGE.opcode, JsonObject( + mapOf( + "chatId" to JsonPrimitive(chatID), + "messageId" to JsonPrimitive(selectedMessageEdit), + "text" to JsonPrimitive(message), + "elements" to JsonArray(emptyList()), + "attachments" to selectedEdit?.attaches!! + ) + ) + ) + + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + var msgID = "" + var msg = Message("", 0L, 0L, JsonArray(emptyList()), "") + + try { + var status = "" + + if (packet.payload.jsonObject["message"]?.jsonObject?.containsKey( + "status" + ) == true + ) { + try { + status = + packet.payload.jsonObject["message"]?.jsonObject["status"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + } + + var textForwarded: String = "" + var senderForwarded: Long = 0L + var msgForwardedID: String = "" + var forwardedAttaches: JsonElement? = JsonNull + var forwardedType: String = "" + + if (packet.payload.jsonObject["message"]!!.jsonObject.contains( + "link" + ) + ) { + val messageLinked = + packet.payload.jsonObject["message"]?.jsonObject["link"]?.jsonObject["message"] + + textForwarded = + messageLinked?.jsonObject["text"]?.jsonPrimitive?.content.toString() + senderForwarded = + messageLinked?.jsonObject["sender"]?.jsonPrimitive!!.long + msgForwardedID = + messageLinked.jsonObject["id"]?.jsonPrimitive!!.long.toString() + forwardedType = + packet.payload.jsonObject["message"]?.jsonObject["link"]?.jsonObject["type"]?.jsonPrimitive?.content.toString() + } + + msg = Message( + packet.payload["message"]?.jsonObject["text"]!!.jsonPrimitive.content, + packet.payload["message"]?.jsonObject["time"]!!.jsonPrimitive.long, + packet.payload["message"]?.jsonObject["sender"]!!.jsonPrimitive.long, + packet.payload["message"]?.jsonObject["attaches"]!!.jsonArray, + status, + MessageLink( + type = forwardedType, msgForLink = msgForLink( + textForwarded, + senderID = senderForwarded, + msgID = msgForwardedID + ) + ) + ) + + msgID = + packet.payload["message"]?.jsonObject["id"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + println(msg) + ChatManager.addMessage(msgID, msg, chatID) + } + }) + onValChange(0L) + onEditChange(0L) + } + } + + message = "" + }) { + Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "send") + } + } + } +} + +@Composable +fun DrawBottomChannel(chatID: Long) { + BottomAppBar() { + TextButton(onClick = {}, modifier = Modifier.fillMaxWidth()) { + Text("Отключить уведомления", fontSize = 25.sp) + } + } +} + +@Composable +fun DrawImages(messages: JsonArray, context : Context, chatId : Long) { + if (messages.size % 2 == 0) { + Column { + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Fixed(2), + modifier = Modifier.heightIn(max = 1000.dp), + userScrollEnabled = false, + ) { + items(messages.size) { index -> + + val photo = messages[index] + val type = photo.jsonObject["_type"]!!.jsonPrimitive.content + + var topstart = 0.dp + var topend = 0.dp + + if (index == 0) topstart = 16.dp + if (index == 1) topend = 16.dp + + if (type == "PHOTO") { + println(photo) + AsyncImage( + model = photo.jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .clip( + RoundedCornerShape( + topStart = topstart, + topEnd = topend, + ) + ) + .fillMaxWidth() + .height(100.dp) + .clickable { + val intent = Intent(context, ImageViewerActivity::class.java) + + intent.putExtra("isSingleImage", false) + intent.putExtra("chatId", chatId) + intent.putExtra( + "pickedPhoto", + photo.jsonObject["photoToken"]!!.jsonPrimitive.content + ) + + context.startActivity(intent) + }, + contentScale = ContentScale.Crop + ) + } + } + } + } + } else if (messages.size == 1) { + if (messages.last().jsonObject["_type"]!!.jsonPrimitive.content == "PHOTO") { + AsyncImage( + model = messages.last().jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .clip( + RoundedCornerShape( + topStart = 16.dp, + topEnd = 16.dp, + ) + ) + .clickable { + val intent = Intent(context, ImageViewerActivity::class.java) + + intent.putExtra("isSingleImage", false) + intent.putExtra("chatId", chatId) + intent.putExtra( + "pickedPhoto", + messages.last().jsonObject["photoToken"]!!.jsonPrimitive.content + ) + + context.startActivity(intent) + }, + + contentScale = ContentScale.Crop + ) + } + } else { + Column { + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Fixed(2), + modifier = Modifier.heightIn(max = 1000.dp), + userScrollEnabled = false, + ) { + items(messages.size - 1) { index -> + + val photo = messages[index] + val type = photo.jsonObject["_type"]!!.jsonPrimitive.content + + if (type == "PHOTO") { + println(photo) + var topstart = 0.dp + var topend = 0.dp + + if (index == 0) topstart = 16.dp + if (index == 1) topend = 16.dp + AsyncImage( + model = photo.jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .clip( + RoundedCornerShape( + topStart = topstart, + topEnd = topend, + ) + ) + .fillMaxWidth() + .height(100.dp) + .clickable { + val intent = Intent(context, ImageViewerActivity::class.java) + + intent.putExtra("isSingleImage", false) + intent.putExtra("chatId", chatId) + intent.putExtra( + "pickedPhoto", + photo.jsonObject["photoToken"]!!.jsonPrimitive.content + ) + + context.startActivity(intent) + }, + contentScale = ContentScale.Crop + ) + } + } + } + + AsyncImage( + model = messages.last().jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .fillMaxWidth() + .clickable { + val intent = Intent(context, ImageViewerActivity::class.java) + + intent.putExtra("isSingleImage", false) + intent.putExtra("chatId", chatId) + intent.putExtra( + "pickedPhoto", + messages.last().jsonObject["photoToken"]!!.jsonPrimitive.content + ) + + context.startActivity(intent) + }, + contentScale = ContentScale.Crop + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/ChatEditActivity.kt b/app/src/main/java/com/sffteam/voidclient/ChatEditActivity.kt new file mode 100644 index 0000000..9a858ff --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ChatEditActivity.kt @@ -0,0 +1,385 @@ +package com.sffteam.voidclient + +import android.net.Uri +import android.os.Bundle +import android.provider.OpenableColumns +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import coil3.compose.AsyncImage +import com.sffteam.voidclient.ui.theme.AppTheme +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.request +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.headers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import java.util.Locale.getDefault + +class ChatEditActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val chatId = intent.getLongExtra("chatId", 0L) + + setContent { + val chats by ChatManager.chatsList.collectAsState() + + var selectedImages by remember { + mutableStateOf>(emptyList()) + } + + val editingChat = chats[chatId] + val avatarUrl = if (selectedImages.isNotEmpty()) { + selectedImages.last().toString() + } else { + editingChat?.avatarUrl + } + + val title = remember { mutableStateOf(editingChat?.title) } + val description = remember { mutableStateOf(editingChat?.description) } + + val context = LocalContext.current + val density = LocalDensity.current + val isShowButton = title.value != editingChat?.title || description.value != editingChat?.description || selectedImages.isNotEmpty() + + val coroutineScope = rememberCoroutineScope() + + val singlePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> + println("uris $uri") + selectedImages = listOf(uri) + } + ) + + AppTheme() { + LazyColumn(modifier = Modifier + .fillMaxSize() + .background(colorScheme.background) + .windowInsetsPadding(WindowInsets.statusBars) + ) { + item { + IconButton({ + finish() + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + "", + modifier = Modifier.size(25.dp), + tint = colorScheme.primary + ) + } + } + item { + Column(verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Box(modifier = Modifier.clickable { + singlePhotoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + }, contentAlignment = Alignment.Center) { + if (avatarUrl?.isNotEmpty() ?: false) { + AsyncImage( + avatarUrl, + "", + modifier = Modifier.size(110.dp) + .clip(CircleShape), + contentScale = ContentScale.FillBounds + ) + } else { + val initial = + title.value?.split(" ")?.mapNotNull { it.firstOrNull() } + ?.take(2)?.joinToString("")?.uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(110.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(title.value!!).first, + Utils.getColorForAvatar(title.value!!).second + ) + ) + ), + ) { + Text( + text = initial!!, + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 22.sp + ) + } + } + + Box( + modifier = Modifier + .background( + colorScheme.primaryContainer, + shape = RoundedCornerShape(8.dp) + ) + .align(Alignment.BottomEnd), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Filled.PhotoCamera, + contentDescription = "Меню", + modifier = Modifier + .size(30.dp) + .align(Alignment.Center), + tint = colorScheme.onPrimaryContainer + ) + } + } + + OutlinedTextField( + value = title.value!!, + onValueChange = { newText -> + title.value = newText + }, + label = { Text("Название чата") }, + textStyle = TextStyle(fontSize = 25.sp), + ) + + OutlinedTextField( + value = description.value!!, + onValueChange = { newText -> + description.value = newText + }, + label = { Text("Описание") }, + textStyle = TextStyle(fontSize = 25.sp), + ) + + AnimatedVisibility(visible = isShowButton, enter = fadeIn(), exit = fadeOut()) { + Button(onClick = { + if (selectedImages.isNotEmpty()) { + // Uhm i think i need another func for it + var uploadedImages = mapOf() + + var imageType = "" + var imageName = "" + val cursor = context.contentResolver.query( + selectedImages.last()!!, null, null, null, null + ) + cursor?.use { + if (it.moveToFirst()) { + val nameIndex = + it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + + imageName = it.getString(nameIndex) + } + } + + val packet = SocketManager.packPacket( + OPCode.UPLOAD_IMAGE.opcode, JsonObject( + mapOf( + "count" to JsonPrimitive(1) + ) + ) + ) + val client = HttpClient(CIO) + + runBlocking { + val imageBytes = try { + context.contentResolver.openInputStream(selectedImages.last()!!) + ?.use { inputStream -> + inputStream.readBytes() + } + } catch (e: Exception) { + null + } + + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + runBlocking { + try { + val response: HttpResponse = + client.post(packet.payload["url"]?.jsonPrimitive?.content.toString()) { + method = HttpMethod.Post + + headers { + append( + HttpHeaders.UserAgent, + "OKMessages/25.12.1 (Android 14; oneplus CPH2465; 382dpi 2300x1023)" + ) + append( + HttpHeaders.ContentType, + "application/octet-stream" + ) + append( + HttpHeaders.ContentDisposition, + "attachment; filename=${imageName}" + ) + append( + "X-Uploading-Mode", + "parallel" + ) + append( + "Content-Range", + "bytes 0-${imageBytes!!.size - 1}/${imageBytes.size}" + ) + append( + HttpHeaders.Connection, + "keep-alive" + ) + append( + HttpHeaders.AcceptEncoding, + "gzip" + ) + } + + setBody(imageBytes) + } + + println(response.request.content) + println("Upload response status: ${response.status}") + val content = + Json.parseToJsonElement(response.bodyAsText()) + + uploadedImages = + content.jsonObject["photos"]!!.jsonObject + + print(content) + + var packetJson = mutableMapOf( + "chatId" to JsonPrimitive(chatId), + "photoToken" to JsonPrimitive(uploadedImages.toList() + .last().second.jsonObject["token"]!!.jsonPrimitive.content), + "theme" to JsonPrimitive(title.value), + "description" to JsonPrimitive(description.value) + ) + val packet = SocketManager.packPacket( + OPCode.EDIT_CHAT_INFO.opcode, + JsonObject(packetJson) + ) + + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + if (packet.payload is JsonObject) { + AccountManager.accountID = + packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["id"]!!.jsonPrimitive.long + + AccountManager.phone = + packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["phone"]!!.jsonPrimitive.content + } + }) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + client.close() + selectedImages = emptyList() + } + } + } + }) + } + } else { + val packet = SocketManager.packPacket(OPCode.EDIT_CHAT_INFO.opcode, + JsonObject(mapOf( + "chatId" to JsonPrimitive(chatId), + "theme" to JsonPrimitive(title.value), + "description" to JsonPrimitive(description.value) + )) + ) + + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + GlobalScope.launch { + ChatManager.processSingleChat(packet.payload["chat"]!!.jsonObject) + } + } + }) + } + } + }, modifier = Modifier.align(Alignment.CenterHorizontally)) { + Text("Сохранить", fontSize = 18.sp) + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/ChatListActivity.kt b/app/src/main/java/com/sffteam/voidclient/ChatListActivity.kt new file mode 100644 index 0000000..a04ba34 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ChatListActivity.kt @@ -0,0 +1,750 @@ +package com.sffteam.voidclient + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Reply +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Bookmark +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.platform.LocalContext +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import com.sffteam.voidclient.preferences.SettingsActivity +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone +import kotlinx.datetime.number +import kotlinx.datetime.toJavaDayOfWeek +import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import java.time.Duration +import java.time.format.TextStyle +import java.util.Date +import java.util.Locale.getDefault +import kotlin.collections.get +import kotlin.time.ExperimentalTime +import kotlin.time.Instant.Companion.fromEpochMilliseconds + +class ChatListActivity : ComponentActivity() { + @OptIn(DelicateCoroutinesApi::class, ExperimentalMaterial3WindowSizeClassApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent @OptIn(ExperimentalMaterial3Api::class) { + Utils.windowSize = calculateWindowSizeClass(this) + AppTheme { + DrawChatList() + } + } + } +} + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class) +@Composable +fun DrawChatList() { + val sheetState = rememberModalBottomSheetState() + var showBottomSheet by remember { mutableStateOf(false) } + var showPopup by remember { mutableStateOf(false) } + + if (showPopup) { + var inputText by remember { mutableStateOf("") } + var errorText by remember { mutableStateOf("") } + + AlertDialog(title = { + Text(text = "Войти в группу") + }, text = { + Column() { + OutlinedTextField( + value = inputText, + onValueChange = { inputText = it }, + label = { Text("Введите ссылку на группу") }, + singleLine = true + ) + + Text( + errorText + ) + } + + }, onDismissRequest = { + showPopup = false + }, confirmButton = { + TextButton( + onClick = { + val urlJoin = inputText.replace("https://max.ru/", "") + val packetSend = SocketManager.packPacket( + OPCode.JOIN_CHAT.opcode, JsonObject( + mapOf( + "link" to JsonPrimitive(urlJoin) + ) + ) + ) + + GlobalScope.launch { + SocketManager.sendPacket(packetSend, callback = { packet -> + if (packet.payload is JsonObject) { + if ("error" in packet.payload) { + errorText = packet.payload["localizedMessage"].toString() + } else { + GlobalScope.launch { + ChatManager.processChats(JsonArray(listOf(packet.payload["chat"]?.jsonObject) as List)) + } + + showPopup = false + showBottomSheet = false + } + } + }) + } + + + }) { + Text("Войти", fontSize = 20.sp) + } + }, dismissButton = { + TextButton( + onClick = { + showPopup = false + }) { + Text("Отмена", fontSize = 20.sp) + } + }) + } + + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, sheetState = sheetState + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(start = 8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + showPopup = true + }) { + Icon( + Icons.Filled.Add, + contentDescription = "edit message", + modifier = Modifier + .padding(end = 10.dp) + .size(20.dp) + .align(Alignment.CenterVertically) + + ) + + Text( + text = "Войти в группу", + fontSize = 25.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + } + } + } + + Column { + val titleSize = when (Utils.windowSize.widthSizeClass) { + WindowWidthSizeClass.Compact -> 18.sp + WindowWidthSizeClass.Medium -> 24.sp + WindowWidthSizeClass.Expanded -> 28.sp + else -> 24.sp + } + + val chats by ChatManager.chatsList.collectAsState() + + val listState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + + val context = LocalContext.current + LaunchedEffect(chats) { + coroutineScope.launch { + listState.scrollToItem(index = chats.size) + } + } + + Scaffold(topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceContainer, + ), title = { + Text( + "Void Client", + fontSize = titleSize, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold + ) + }, navigationIcon = { + IconButton({ + val intent = Intent(context, SettingsActivity::class.java) + + context.startActivity(intent) + }) { + Icon( + Icons.Filled.Settings, contentDescription = "Меню" + ) + } + }, actions = { + IconButton({ showBottomSheet = true }) { + Icon( + Icons.Filled.Add, contentDescription = "Добавить чат" + ) + } + IconButton({ }) { Icon(Icons.Filled.Search, contentDescription = "Поиск") } + }, modifier = Modifier.heightIn(max = 200.dp) + + ) + }) { + LazyColumn(reverseLayout = true, state = listState, modifier = Modifier.padding(it)) { + items(chats.entries.toList().sortedBy { (_, value) -> + value.messages.entries.toList() + .maxByOrNull { (_, value) -> value.sendTime }!!.value.sendTime + }, key = { entry -> + entry.key + }) { entry -> + println(entry) + DrawUser(entry.key, entry.value, LocalContext.current, Modifier.weight(0.5f)) + } + } + } + } +} + +@OptIn(ExperimentalTime::class) +@Composable +fun DrawUser(chatID: Long, chat: Chat, context: Context, modifier: Modifier) { + var chatTitle: String + var chatIcon: String + val users by UserManager.usersList.collectAsState() + var secondUser = 0L + val sortedMessages = chat.messages.entries.toList().sortedBy { (_, value) -> value.sendTime } + + val lastMessage = sortedMessages.last().value + + var lastMsgUser = "" + + if (chat.type == "CHAT") { + if (lastMessage.senderID == AccountManager.accountID) { + lastMsgUser = "Вы" + } else { + lastMsgUser = + users[lastMessage.senderID]?.firstName.toString() + + if (users[lastMessage.senderID]?.lastName?.isNotEmpty() + ?: false + ) { + lastMsgUser += " " + users[lastMessage.senderID]?.lastName + } + } + } + + if (chat.type == "DIALOG" && chatID != 0L) { + for (i in chat.users.toList()) { + if (i.first != AccountManager.accountID) { + secondUser = i.first + break + } + } + val user = users[secondUser] + + chatTitle = user?.firstName + " " + user?.lastName + chatIcon = user?.avatarUrl.toString() + } else { + chatTitle = chat.title + chatIcon = chat.avatarUrl + } + + Box( + modifier = modifier + .height(80.dp) + .fillMaxWidth() + .clickable { + val intent = Intent(context, ChatActivity::class.java) + intent.putExtra("chatTitle", chatTitle) + intent.putExtra("chatIcon", chatIcon) + intent.putExtra("chatID", chatID) + intent.putExtra("messageTime", lastMessage.sendTime) + intent.putExtra("chatType", chat.type) + + context.startActivity(intent) + } + .background(Color.Transparent) + .padding(start = 12.dp, end = 16.dp), + contentAlignment = Alignment.Center) { + val fontTitleSize = when (Utils.windowSize.widthSizeClass) { + WindowWidthSizeClass.Compact -> 20.sp + WindowWidthSizeClass.Medium -> 24.sp + WindowWidthSizeClass.Expanded -> 30.sp + else -> 24.sp + } + + val fontSize = when (Utils.windowSize.widthSizeClass) { + WindowWidthSizeClass.Compact -> 18.sp + WindowWidthSizeClass.Medium -> 24.sp + WindowWidthSizeClass.Expanded -> 28.sp + else -> 24.sp + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + + if (chatIcon.isNotEmpty()) { + AsyncImage( + model = chatIcon, + contentDescription = "ChatIcon", + modifier = Modifier + .width(60.dp) + .height(60.dp) + .clip(CircleShape), + contentScale = ContentScale.FillBounds + ) + } else if (chatID == 0L) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(60.dp) + .height(60.dp) + .clip(CircleShape) + .background(colorScheme.primaryContainer), + ) { + Icon( + Icons.Filled.Bookmark, + contentDescription = "edit message", + modifier = Modifier + .size(30.dp) + .align(Alignment.Center), + tint = colorScheme.onPrimaryContainer + ) + } + } else { + val initial = + chatTitle.split(" ").mapNotNull { it.firstOrNull() }.take(2).joinToString("") + .uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(60.dp) + .height(60.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(chatTitle).first, + Utils.getColorForAvatar(chatTitle).second + ) + ) + ), + + ) { + Text( + text = initial, + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 25.sp + ) + } + } + + Column() { + Row(modifier = Modifier.fillMaxWidth()) { + val lastMessageTime = lastMessage.sendTime + val currentTime = Date().time + + val instantLast = fromEpochMilliseconds(lastMessageTime) + + val duration = Duration.ofSeconds(currentTime / 1000 - lastMessageTime / 1000) + + val localDateTime = instantLast.toLocalDateTime(TimeZone.currentSystemDefault()) + + val hours = if (localDateTime.hour < 10) { + "0${localDateTime.hour}" + } else { + localDateTime.hour + } + + val minutes = if (localDateTime.minute < 10) { + "0${localDateTime.minute}" + } else { + localDateTime.minute + } + + val dayOfWeek = localDateTime.dayOfWeek.toJavaDayOfWeek().getDisplayName( + TextStyle.SHORT, getDefault() + ) + + val time = if (duration.toHours() < 24) { + "${hours}:${minutes}" + } else if (duration.toHours() >= 24 && duration.toDays() < 7) { + dayOfWeek.toString().replaceFirstChar { it.titlecase(getDefault()) } + } else { + "${localDateTime.day}.${localDateTime.month.number}.${localDateTime.year}" + } + + Text( + chatTitle, + fontSize = fontTitleSize, + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + fontWeight = FontWeight.Bold, + modifier = Modifier + .weight(0.45f) + .padding(end = 4.dp) + ) + + if (lastMessage.message.isNotEmpty() || lastMessage.attaches != JsonNull) { + Text( + text = time, fontSize = 16.sp, modifier = Modifier.alpha(0.7f) + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + if (lastMessage.attaches is JsonArray && !(lastMessage.attaches?.jsonArray?.isNotEmpty() == true && lastMessage.attaches.jsonArray?.last()?.jsonObject?.contains( + "event" + ) == true) + ) { + if (chat.type == "CHAT") { + lastMsgUser += ": " + } + + val lastMSGCleared = if (lastMessage.link.type == "FORWARD") { + lastMessage.link.msgForLink.message.replace("\n", " ") + } else { + lastMessage.message.replace("\n", " ") + } + + val annotatedString = buildAnnotatedString { + append(lastMsgUser) + + if (lastMessage.attaches?.jsonArray?.isNotEmpty() ?: false) { + lastMessage.attaches.jsonArray.forEachIndexed { index, jsonelement -> + val type = + jsonelement.jsonObject["_type"]!!.jsonPrimitive.content + + if (type == "PHOTO") { + val imageId = "image_$index" + appendInlineContent(id = imageId) + append(" ") + } + } + } + if (lastMessage.link.msgForLink.attaches is JsonArray && lastMessage.link.type == "FORWARD") { + lastMessage.link.msgForLink.attaches.jsonArray.forEachIndexed { index, jsonelement -> + val type = + jsonelement.jsonObject["_type"]!!.jsonPrimitive.content + + if (type == "PHOTO") { + val imageId = "image_$index" + appendInlineContent(id = imageId) + append(" ") + } + } + } + + append(lastMSGCleared) + + if (lastMessage.link.type == "FORWARD") { + appendInlineContent(id = "iconId") + } + } + + val inlineContentMap = mutableMapOf( + + ) + val placeholder = Placeholder( + width = 25.sp, + height = 25.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter + ) + + if (lastMessage.attaches?.jsonArray?.isNotEmpty() ?: false) { + lastMessage.attaches.jsonArray.forEachIndexed { index, jsonelement -> + val type = jsonelement.jsonObject["_type"]!!.jsonPrimitive.content + + if (type == "PHOTO") { + val imageId = "image_$index" + inlineContentMap[imageId] = + InlineTextContent(placeholder) { _ -> + AsyncImage( + model = jsonelement.jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .size(25.dp) + .clip(RoundedCornerShape(2.dp)), + contentScale = ContentScale.Crop + ) + } + } + } + } + + if (lastMessage.link.type == "FORWARD" && lastMessage.link.msgForLink.attaches is JsonArray) { + lastMessage.link.msgForLink.attaches.jsonArray.forEachIndexed { index, jsonelement -> + println("sh1t jsonelm $jsonelement") + val type = jsonelement.jsonObject["_type"]!!.jsonPrimitive.content + + if (type == "PHOTO") { + val imageId = "image_$index" + inlineContentMap[imageId] = + InlineTextContent(placeholder) { _ -> + AsyncImage( + model = jsonelement.jsonObject["baseUrl"]!!.jsonPrimitive.content, + contentDescription = "ChatIcon", + modifier = Modifier + .size(25.dp) + .clip(RoundedCornerShape(2.dp)), + contentScale = ContentScale.Crop + ) + } + } + } + } + + if (lastMessage.link.type == "FORWARD") { + inlineContentMap["iconId"] = InlineTextContent(placeholder) { _ -> + Icon( + imageVector = Icons.AutoMirrored.Filled.Reply, + contentDescription = "forwardIcon", + ) + } + } + Text( + text = annotatedString, + fontSize = fontSize, + inlineContent = inlineContentMap, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.alpha(0.7f) + ) + } else { + var text = "" + if (lastMessage.attaches != JsonNull) { + val attach = lastMessage.attaches?.jsonArray?.last() + + val event = attach?.jsonObject["event"]?.jsonPrimitive?.content + + when (event) { + "remove" -> { + val peoplesRemoved = + attach?.jsonObject["userId"]?.jsonPrimitive?.long + + UserManager.checkForExisting(peoplesRemoved!!) + UserManager.checkForExisting(lastMessage.senderID) + + var whomAdded = users[peoplesRemoved]?.firstName.toString() + + if (users[peoplesRemoved]?.lastName?.isNotEmpty() == true) { + whomAdded += " " + users[peoplesRemoved]?.lastName + } + if (lastMessage.senderID == AccountManager.accountID) { + text += "Вы удалили $whomAdded" + } else { + var whoAdded = + users[lastMessage.senderID]?.firstName.toString() + + if (users[lastMessage.senderID]?.firstName?.isNotEmpty() == true) { + whoAdded += " " + users[lastMessage.senderID]?.lastName + } + + text += "$whoAdded удалил(-а) $whomAdded" + } + } + + "new" -> { + UserManager.checkForExisting(lastMessage.senderID) + if (chat.type != "CHANNEL") { + if (lastMessage.senderID == AccountManager.accountID) { + text += "$lastMsgUser создали чат" + } else { + text += "$lastMsgUser создал(-а) чат" + } + } else { + text += "Канал создан" + } + } + + "add" -> { + val peoplesAdded = attach.jsonObject["userIds"]?.jsonArray + + for (i in peoplesAdded!!) { + if (attach.jsonObject["userIds"]?.jsonArray?.isNotEmpty() == true) { + UserManager.checkForExisting(i.jsonPrimitive.long) + } + } + + UserManager.checkForExisting(lastMessage.senderID) + + if (lastMessage.senderID == AccountManager.accountID) { + text += "Вы добавили " + } else { + var whoAdded = + users[lastMessage.senderID]?.firstName.toString() + + if (users[lastMessage.senderID]?.firstName?.isNotEmpty() == true) { + whoAdded += " " + users[lastMessage.senderID]?.lastName + } + + text += "$whoAdded добавил(-а) " + } + + for (i in peoplesAdded) { + var whomAdded = + users[i.jsonPrimitive.long]?.firstName.toString() + + if (users[i.jsonPrimitive.long]?.lastName?.isNotEmpty() == true) { + whomAdded += " " + users[i.jsonPrimitive.long]?.lastName + } + + text += whomAdded + if (i != peoplesAdded.last()) { + text += ", " + } + } + } + + "icon" -> { + UserManager.checkForExisting(lastMessage.senderID) + + if (lastMessage.senderID == AccountManager.accountID) { + text += "$lastMsgUser изменили фото чата" + } else { + text += "$lastMsgUser изменил(-а) фото чата" + } + } + + "title" -> { + UserManager.checkForExisting(lastMessage.senderID) + val newTitle = + attach.jsonObject["title"]?.jsonPrimitive?.content + + if (lastMessage.senderID == AccountManager.accountID) { + text += "$lastMsgUser изменили название чата на «$newTitle»" + } else { + text += "$lastMsgUser изменил(-а) название чата на «$newTitle»" + } + } + + "leave" -> { + UserManager.checkForExisting(lastMessage.senderID) + + if (lastMessage.senderID == AccountManager.accountID) { + text += "Вы покинули чат" + } else { + text += "$lastMsgUser покинул(-а) чат" + } + } + + "joinByLink" -> { + UserManager.checkForExisting(attach.jsonObject["userId"]?.jsonPrimitive?.long!!) + + if (lastMessage.senderID == AccountManager.accountID) { + text += "Вы присоединились к чату" + } else { + text += "$lastMsgUser присоединился(-ась) к чату" + } + } + + "system" -> { + text += attach.jsonObject["message"]?.jsonPrimitive?.content + } + } + Text( + text = text, + fontSize = fontSize, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.alpha(0.7f) + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/ChatManager.kt b/app/src/main/java/com/sffteam/voidclient/ChatManager.kt new file mode 100644 index 0000000..5ca7ff0 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ChatManager.kt @@ -0,0 +1,641 @@ +package com.sffteam.voidclient + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlin.collections.plus + +data class msgForLink( + val message: String = "", + val sendTime: Long = 0L, + val senderID: Long = 0L, + val attaches: JsonElement? = JsonArray(emptyList()), + val status: String = "", + val msgID: String = "" +) + +data class MessageLink( + val msgForLink: msgForLink = msgForLink(), val type: String = "" +) + +data class Message( + val message: String = "", + val sendTime: Long = 0L, + val senderID: Long = 0L, + val attaches: JsonElement? = JsonArray(emptyList()), + val status: String = "", + val link: MessageLink = MessageLink() +) + +data class Chat( + val avatarUrl: String, + val title: String, + val messages: Map, + val type: String, + val users: Map, + val usersCount: Int, + val needGetMessages: Boolean = true, + val description: String = "", + val admins : List = emptyList(), + val owner : Long = 0L, + val inviteLink : String = "", + val pinned : Int = 0, +) + +object ChatManager { + private val _chatsList = MutableStateFlow>(emptyMap()) + var chatsList = _chatsList.asStateFlow() + + fun clearChatsList() { + _chatsList.update { + emptyMap() + } + } + fun removeMessage(chatID: Long, messageID: String) { + _chatsList.update { oldMap -> + oldMap + (chatID to Chat( + oldMap[chatID]?.avatarUrl ?: "", + oldMap[chatID]?.title ?: "", + oldMap[chatID]?.messages?.minus(messageID) ?: emptyMap(), + oldMap[chatID]?.type ?: "", + oldMap[chatID]?.users ?: emptyMap(), + oldMap[chatID]?.usersCount ?: 0, + oldMap[chatID]?.needGetMessages ?: false, + oldMap[chatID]?.description ?: "", + oldMap[chatID]?.admins ?: emptyList(), + oldMap[chatID]?.owner ?: 0L, + oldMap[chatID]?.inviteLink ?: "", + + )) + } + } + + fun addMessage(messageID: String, message: Message, chatID: Long) { + try { + _chatsList.update { oldMap -> + oldMap + (chatID to Chat( + oldMap[chatID]?.avatarUrl ?: "", + oldMap[chatID]?.title ?: "", + oldMap[chatID]?.messages?.plus(mapOf(messageID to message)) ?: emptyMap(), + oldMap[chatID]?.type ?: "", + oldMap[chatID]?.users ?: emptyMap(), + oldMap[chatID]?.usersCount ?: 0, + oldMap[chatID]?.needGetMessages ?: false, + oldMap[chatID]?.description ?: "", + oldMap[chatID]?.admins ?: emptyList(), + oldMap[chatID]?.owner ?: 0L, + oldMap[chatID]?.inviteLink ?: "", + )) + } + } catch (e: Exception) { + println(e) + } + } + + fun processMessages(messages: JsonArray, chatID: Long) { + val msgList: MutableMap = mutableMapOf() + + for (i in messages) { + var message = Message() + var msg = "" + var sendtime = 0L + var senderID = 0L + var attachs = JsonArray(emptyList()) + var status = "" + var msgID = "" + var desc = "" + + var textForwarded = "" + var senderForwarded = 0L + var msgForwardedID = "" + var forwardedAttaches: JsonElement? = JsonNull + var forwardedType = "" + + try { + msg = i.jsonObject["text"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + try { + sendtime = i.jsonObject["time"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + try { + senderID = i.jsonObject["sender"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + try { + attachs = i.jsonObject["attaches"]!!.jsonArray + } catch (e: Exception) { + println(e) + } + + try { + status = i.jsonObject["status"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + try { + msgID = i.jsonObject["id"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + if (i.jsonObject.contains("link")) { + try { + val msgLink = i.jsonObject["link"] + forwardedType = msgLink!!.jsonObject["type"]?.jsonPrimitive!!.content + textForwarded = + msgLink.jsonObject["message"]!!.jsonObject["text"]!!.jsonPrimitive.content + senderForwarded = + msgLink.jsonObject["message"]!!.jsonObject["sender"]!!.jsonPrimitive.long + msgForwardedID = + msgLink.jsonObject["message"]!!.jsonObject["id"]!!.jsonPrimitive.content + forwardedAttaches = msgLink.jsonObject["message"]!!.jsonObject["attaches"] + } catch (e: Exception) { + println(e) + } + } + + message = message.copy( + message = msg, + sendTime = sendtime, + senderID = senderID, + attaches = attachs, + status = status, + link = MessageLink( + type = forwardedType, msgForLink = msgForLink( + message = textForwarded, + senderID = senderForwarded, + attaches = forwardedAttaches, + msgID = msgForwardedID, + ) + ), + ) + + println("coolmsg $message") + msgList[msgID] = message + } + + println("notcool size=${msgList.size}") + _chatsList.update { oldMap -> + oldMap + (chatID to Chat( + oldMap[chatID]?.avatarUrl ?: "", + oldMap[chatID]?.title ?: "", + oldMap[chatID]?.messages?.plus(msgList) ?: emptyMap(), + oldMap[chatID]?.type ?: "", + oldMap[chatID]?.users ?: emptyMap(), + oldMap[chatID]?.usersCount ?: 0, + if (msgList.size == 30) true else false, + oldMap[chatID]?.description ?: "", + oldMap[chatID]?.admins ?: emptyList(), + oldMap[chatID]?.owner ?: 0L, + oldMap[chatID]?.inviteLink ?: "", + )) + } + println(_chatsList.value[chatID]?.messages?.size) + } + + suspend fun removeChat(chatId : Long) { + val updatedChatsList = _chatsList.value.toMutableMap() + + updatedChatsList.remove(chatId) + + _chatsList.update { + updatedChatsList + } + } + + suspend fun processSingleChat(chat : JsonObject) { + var chatID: Long = 0 + + try { + chatID = chat.jsonObject["id"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + println(chatID) + + try { + println("chat $chat") + var lastmsgtm = 0L + var msgID = "" + var lastmsg = "" + var avatarUrl = "" + var status = "" + var title = "" + var senderID = 0L + var type = "" + var users = mutableMapOf() + var attaches: JsonElement? = JsonNull + var usersCount = 0 + var desc = "" + var owner = 0L + var admins: MutableList = mutableListOf() + var inviteLink = "" + + var textForwarded = "" + var senderForwarded = 0L + var msgForwardedID = "" + var forwardedAttaches: JsonElement? = JsonNull + var forwardedType = "" + + try { + lastmsgtm = + chat.jsonObject["lastMessage"]!!.jsonObject["time"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + try { + lastmsg = + chat.jsonObject["lastMessage"]!!.jsonObject["text"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + try { + senderID = + chat.jsonObject["lastMessage"]!!.jsonObject["sender"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + try { + status = + chat.jsonObject["lastMessage"]!!.jsonObject["status"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + try { + msgID = chat.jsonObject["lastMessage"]!!.jsonObject["id"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + + if (chat.jsonObject["lastMessage"]!!.jsonObject.contains("attaches")) { + try { + attaches = chat.jsonObject["lastMessage"]!!.jsonObject["attaches"] + } catch (e: Exception) { + println(e) + } + } else { + attaches = JsonArray(emptyList()) + } + + if (chat.jsonObject["lastMessage"]!!.jsonObject.contains("link")) { + try { + val msgLink = chat.jsonObject["lastMessage"]!!.jsonObject["link"] + forwardedType = msgLink!!.jsonObject["type"]?.jsonPrimitive!!.content + textForwarded = + msgLink.jsonObject["message"]!!.jsonObject["text"]!!.jsonPrimitive.content + senderForwarded = + msgLink.jsonObject["message"]!!.jsonObject["sender"]!!.jsonPrimitive.long + msgForwardedID = + msgLink.jsonObject["message"]!!.jsonObject["id"]!!.jsonPrimitive.content + forwardedAttaches = msgLink.jsonObject["message"]!!.jsonObject["attaches"] + } catch (e: Exception) { + println(e) + } + } + + if (chat.jsonObject.contains("baseIconUrl")) { + try { + avatarUrl = chat.jsonObject["baseIconUrl"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + } + + if (chat.jsonObject.contains("title")) { + try { + title = chat.jsonObject["title"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + } else { + if (chatID == 0L) { + title = "Избранное" + } + } + + try { + type = chat.jsonObject["type"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + try { + for (i in chat.jsonObject["participants"]?.jsonObject?.toList()!!) { + users[i.first.toLong()] = i.second.jsonPrimitive.long + } + } catch (e: Exception) { + println(e) + } + + if (chat.jsonObject.contains("participantsCount")) { + try { + usersCount = chat.jsonObject["participantsCount"]!!.jsonPrimitive.int + } catch (e: Exception) { + println(e) + } + } + + + if (chat.jsonObject.contains("description")) { + desc = chat.jsonObject["description"]?.jsonPrimitive?.content!! + } + + if (chat.jsonObject.contains("owner")) { + owner = chat.jsonObject["owner"]?.jsonPrimitive?.long!! + } + + if (chat.jsonObject.contains("admins")) { + for (y in chat.jsonObject["admins"]?.jsonArray?.toList()!!) { + admins += y.jsonPrimitive.long + } + } + + if (chat.jsonObject.contains("link")) { + inviteLink = chat.jsonObject["link"]?.jsonPrimitive?.content!! + } + + val messages: Map = mapOf( + msgID to Message( + lastmsg, lastmsgtm, senderID, attaches, status, link = MessageLink( + type = forwardedType, msgForLink = msgForLink( + textForwarded, + senderID = senderForwarded, + attaches = forwardedAttaches, + msgID = msgForwardedID + ) + ) + ) + ) + + _chatsList.update { oldMap -> + oldMap.toMap() + (chatID to Chat( + avatarUrl, + title, + if (oldMap[chatID]?.messages?.isNotEmpty() == true) oldMap[chatID]?.messages?.plus( + messages + )!! else messages, + type, + users, + usersCount, + oldMap[chatID]?.needGetMessages ?: true, + desc, + admins, + owner, + inviteLink, + )) + } + } catch (e : Exception) { + println(e) + } + } + /* Function. */ + suspend fun processChats(chats: JsonArray): Boolean { + val userIds = mutableListOf() + + for (i in chats) { + var chatID: Long = 0 + + try { + chatID = i.jsonObject["id"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + println(chatID) + + try { + println("chat $i") + var lastmsgtm = 0L + var msgID = "" + var lastmsg = "" + var avatarUrl = "" + var status = "" + var title = "" + var senderID = 0L + var type = "" + var users = mutableMapOf() + var attaches: JsonElement? = JsonNull + var usersCount = 0 + var desc = "" + var owner = 0L + var admins : MutableList = mutableListOf() + var inviteLink = "" + + var textForwarded = "" + var senderForwarded = 0L + var msgForwardedID = "" + var forwardedAttaches: JsonElement? = JsonNull + var forwardedType = "" + + if (i.jsonObject.contains("lastMessage")) { + try { + lastmsgtm = + i.jsonObject["lastMessage"]!!.jsonObject["time"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + try { + lastmsg = + i.jsonObject["lastMessage"]!!.jsonObject["text"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + try { + senderID = + i.jsonObject["lastMessage"]!!.jsonObject["sender"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + try { + status = + i.jsonObject["lastMessage"]!!.jsonObject["status"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + try { + msgID = i.jsonObject["lastMessage"]!!.jsonObject["id"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + + if (i.jsonObject["lastMessage"]!!.jsonObject.contains("attaches")) { + try { + attaches = i.jsonObject["lastMessage"]!!.jsonObject["attaches"] + } catch (e: Exception) { + println(e) + } + } else { + attaches = JsonArray(emptyList()) + } + + if (i.jsonObject["lastMessage"]!!.jsonObject.contains("link")) { + try { + val msgLink = i.jsonObject["lastMessage"]!!.jsonObject["link"] + forwardedType = msgLink!!.jsonObject["type"]?.jsonPrimitive!!.content + textForwarded = + msgLink.jsonObject["message"]!!.jsonObject["text"]!!.jsonPrimitive.content + senderForwarded = + msgLink.jsonObject["message"]!!.jsonObject["sender"]!!.jsonPrimitive.long + msgForwardedID = + msgLink.jsonObject["message"]!!.jsonObject["id"]!!.jsonPrimitive.content + forwardedAttaches = msgLink.jsonObject["message"]!!.jsonObject["attaches"] + } catch (e: Exception) { + println(e) + } + } + } + + if (i.jsonObject.contains("baseIconUrl")) { + try { + avatarUrl = i.jsonObject["baseIconUrl"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + } + + if (i.jsonObject.contains("title")) { + try { + title = i.jsonObject["title"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + } else { + if (chatID == 0L) { + title = "Избранное" + } + } + + try { + type = i.jsonObject["type"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + } + + print("pediki ${i.jsonObject["participants"]}") + try { + for (i in i.jsonObject["participants"]?.jsonObject?.toList()!!) { + println("partip ${i}") + userIds += Json.encodeToJsonElement(Long.serializer(), i.first.toLong()) + users[i.first.toLong()] = i.second.jsonPrimitive.long + } + } catch (e: Exception) { + println("pizda") + println(e) + } + + if (i.jsonObject.contains("participantsCount")) { + try { + usersCount = i.jsonObject["participantsCount"]!!.jsonPrimitive.int + } catch (e: Exception) { + println(e) + } + } + + + if (i.jsonObject.contains("description")) { + desc = i.jsonObject["description"]?.jsonPrimitive?.content!! + } + + if (i.jsonObject.contains("owner")) { + owner = i.jsonObject["owner"]?.jsonPrimitive?.long!! + } + + if (i.jsonObject.contains("admins")) { + for (y in i.jsonObject["admins"]?.jsonArray?.toList()!!) { + admins += y.jsonPrimitive.long + } + } + + if (i.jsonObject.contains("link")) { + inviteLink = i.jsonObject["link"]!!.jsonPrimitive.content + } + + val messages: Map = mapOf( + msgID to Message( + lastmsg, lastmsgtm, senderID, attaches, status, link = MessageLink( + type = forwardedType, msgForLink = msgForLink( + textForwarded, + senderID = senderForwarded, + attaches = forwardedAttaches, + msgID = msgForwardedID + ) + ) + ) + ) + + _chatsList.update { oldMap -> + oldMap.toMap() + (chatID to Chat( + avatarUrl, + title, + if (oldMap[chatID]?.messages?.isNotEmpty() == true) oldMap[chatID]?.messages?.plus(messages)!! else messages, + type, + users, + usersCount, + oldMap[chatID]?.needGetMessages ?: true, + desc, + admins, + owner, + inviteLink, + )) + } + + println("current chat ${_chatsList.value[chatID]}") + } catch (e: Exception) { + println(e) + } + } + + val packet = SocketManager.packPacket( + OPCode.CONTACTS_INFO.opcode, JsonObject( + mapOf( + "contactIds" to JsonArray(userIds), + ) + ) + ) + + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + GlobalScope.launch { + UserManager.processUsers(packet.payload["contacts"]!!.jsonArray) + } + } + }) + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/ChatViewActivity.kt b/app/src/main/java/com/sffteam/voidclient/ChatViewActivity.kt new file mode 100644 index 0000000..3297bf4 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ChatViewActivity.kt @@ -0,0 +1,470 @@ +package com.sffteam.voidclient + +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.automirrored.filled.ExitToApp +import androidx.compose.material.icons.filled.ArrowBackIos +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.ExitToApp +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import coil3.compose.AsyncImage +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import java.util.Locale.getDefault +import kotlin.time.ExperimentalTime +import kotlin.time.Instant.Companion.fromEpochMilliseconds + +class ChatViewActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val chatId = intent.getLongExtra("chatId", 0L) + setContent { + val chats by ChatManager.chatsList.collectAsState() + val users by UserManager.usersList.collectAsState() + + val viewedChat = chats[chatId] + val usersInChat = if (viewedChat?.users?.isNotEmpty() == true) { + viewedChat.users + } else { + null + } + val description = chats[chatId]?.description + val inviteLink = chats[chatId]?.inviteLink + val owner = chats[chatId]?.owner + val type = viewedChat?.type + val avatarUrl = viewedChat?.avatarUrl + + val coroutineScope = rememberCoroutineScope() + val clipboardManager = LocalClipboard.current + val context = LocalContext.current + + AppTheme { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(colorScheme.background) + .windowInsetsPadding(WindowInsets.statusBars) + ) { + item { + Row() { + IconButton({ + finish() + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + "", + modifier = Modifier.size(25.dp), + tint = colorScheme.primary + ) + } + + if (AccountManager.accountID == owner) { + Spacer(modifier = Modifier.weight(1f)) + + IconButton({ + val intent = Intent(context, ChatEditActivity::class.java) + intent.putExtra("chatId", chatId) + + context.startActivity(intent) + }) { + Icon( + Icons.Filled.Edit, + "", + modifier = Modifier.size(25.dp), + tint = colorScheme.primary + ) + } + } + } + } + + item { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (avatarUrl?.isNotEmpty() == true) { + AsyncImage( + avatarUrl, + "", + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(110.dp) + .clip(CircleShape) + .clickable { + val intent = Intent(context, ImageViewerActivity::class.java) + + intent.putExtra("isSingleImage", true) + intent.putExtra("image", avatarUrl) + + context.startActivity(intent) + } + ) + } else { + val initial = + viewedChat?.title?.split(" ")?.mapNotNull { it.firstOrNull() } + ?.take(2)?.joinToString("")?.uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(100.dp) + .height(100.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(viewedChat?.title.toString()).first, + Utils.getColorForAvatar(viewedChat?.title.toString()).second + ) + ) + ) + .align(Alignment.CenterHorizontally), + ) { + Text( + text = initial.toString(), + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 40.sp + ) + } + } + + Text( + viewedChat?.title.toString(), + fontSize = 24.sp, + modifier = Modifier.align(Alignment.CenterHorizontally).padding(start = 8.dp), + color = colorScheme.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + var desc = "" + when (type) { + "CHAT" -> { + val sizeString = viewedChat.users.size.toString() + val sizeStringLast = sizeString.last().code + desc = when (sizeStringLast) { + 1 -> { + "Тут только вы" + } + + else -> { + if (sizeStringLast == 2 || sizeStringLast == 3 || sizeStringLast == 4) { + "$sizeString участника" + } else if (sizeStringLast == 1 && sizeString != "11") { + "$sizeString участник" + } else { + "$sizeString участников" + } + } + } + + } + + "CHANNEL" -> { + desc = if (viewedChat.usersCount.toString() + .last().code == 2 || viewedChat.usersCount.toString() + .last().code == 3 || viewedChat.usersCount.toString() + .last().code == 4 + ) { + println( + "code ${ + viewedChat.usersCount.toString().last().code + }" + ) + viewedChat.usersCount.toString() + " подписчика" + } else if (viewedChat.usersCount.toString() + .last().code == 1 && viewedChat.usersCount.toString() != "11" + ) { + viewedChat.usersCount.toString() + " подписчик" + } else { + viewedChat.usersCount.toString() + " подписчиков" + } + } + } + + Text( + desc, + fontSize = 18.sp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .alpha(0.7f), + color = colorScheme.secondary + ) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + modifier = Modifier.padding(start = 8.dp).fillMaxWidth()) { + Button(onClick = { + val packet = SocketManager.packPacket( + OPCode.LEAVE_CHAT.opcode, JsonObject( + mapOf( + "chatId" to JsonPrimitive(chatId) + ) + ) + ) + + coroutineScope.launch { + SocketManager.sendPacket(packet, { + + }) + } + + val intent = Intent(context, ChatListActivity::class.java) + + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + + context.startActivity(intent) + finish() + }, modifier = Modifier.padding(4.dp)) { + Column() { + Icon( + Icons.AutoMirrored.Filled.ExitToApp, + "", + modifier = Modifier.size(25.dp).align(Alignment.CenterHorizontally) + ) + Text("Покинуть группу", fontSize = 18.sp, modifier = Modifier.align(Alignment.CenterHorizontally)) + } + } + } + } + } + + item { + if (description?.isNotEmpty() == true) { + Box(modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .background(colorScheme.secondaryContainer, RoundedCornerShape(16.dp)) + ) { + Column(modifier = Modifier.padding(8.dp)) { + Text("Описание", modifier = Modifier.alpha(0.7f), + color = colorScheme.onSecondaryContainer, fontSize = 14.sp) + + Text(description, color = colorScheme.onSecondaryContainer, fontSize = 18.sp) + } + } + } + } + + item { + if (inviteLink?.isNotEmpty() == true) { + Box(modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .background(colorScheme.secondaryContainer, RoundedCornerShape(16.dp)) + ) { + Column(modifier = Modifier.padding(8.dp) + .clickable { + coroutineScope.launch { + clipboardManager.setClipEntry( + ClipEntry( + ClipData.newPlainText( + inviteLink, inviteLink + ) + ) + ) + } + }) { + Text("Ссылка для приглашения в чат", modifier = Modifier.alpha(0.7f), + color = colorScheme.onSecondaryContainer, fontSize = 14.sp) + + Text(inviteLink, color = colorScheme.onSecondaryContainer, fontSize = 18.sp) + } + } + } + } + + item { + Box(modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .background(colorScheme.secondaryContainer, RoundedCornerShape(16.dp)) + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row(modifier = Modifier.padding(bottom = 8.dp)) { + Text("Участники", color = colorScheme.onSecondaryContainer, fontSize = 22.sp) + } + + if (usersInChat?.isNotEmpty() == true) { + for (user in usersInChat?.toList()?.sortedByDescending { it.second }!!) { + val userIn = users[user.first] + + if (userIn != null) { + DrawUser(userIn, user.first, viewedChat!!.owner, viewedChat.admins, user.second, context) + } else { + UserManager.checkForExisting(user.first) + } + } + } + } + } + } + } + } + } + } +} + +@OptIn(ExperimentalTime::class) +@Composable +fun DrawUser(user : User, userId : Long, owner : Long, admins : List, time : Long, context : Context) { + val fullName = user.firstName + if (user.lastName.isNotEmpty()) { + " " + user.lastName + } else { + "" + } + + val userRole = if (userId == owner) { + "Владелец" + } else { + var admin = "" + for (i in admins) { + if (userId == i) { + admin = "Администратор" + break + } + } + admin + } + + val lastSeen = fromEpochMilliseconds(time) + + val localLastSeen = + lastSeen.toLocalDateTime(TimeZone.currentSystemDefault()) + + Box(modifier = Modifier.padding(bottom = 8.dp).clickable { + val intent = Intent(context, ProfileViewActivity::class.java) + + intent.putExtra("userId", userId) + + context.startActivity(intent) + }) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)) { + if (user.avatarUrl.isNotEmpty()) { + AsyncImage( + user.avatarUrl, + "", + modifier = Modifier.size(45.dp) + .clip(CircleShape), + contentScale = ContentScale.FillBounds + ) + } else { + val initial = + fullName.split(" ").mapNotNull { it.firstOrNull() } + .take(2).joinToString("").uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(45.dp) + .height(45.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(fullName).first, + Utils.getColorForAvatar(fullName).second + ) + ) + ), + ) { + Text( + text = initial, + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 22.sp + ) + } + } + + Column(verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = Modifier.weight(0.5f)) { + Text(fullName, + fontSize = 22.sp, + color = colorScheme.onSecondaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text("Был(а) недавно", + fontSize = 16.sp, + color = colorScheme.onSecondaryContainer, + modifier = Modifier.alpha(0.7f) + ) + } + + if (userRole.isNotEmpty()) { + + Text(userRole, + fontSize = 18.sp, + color = colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/CodeActivity.kt b/app/src/main/java/com/sffteam/voidclient/CodeActivity.kt new file mode 100644 index 0000000..8b7d019 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/CodeActivity.kt @@ -0,0 +1,170 @@ +package com.sffteam.voidclient + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +val Context.dataStore: DataStore by preferencesDataStore(name = "token") + +class CodeActivity : ComponentActivity() { + @OptIn(DelicateCoroutinesApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + AppTheme { + val code = remember { mutableStateOf("") } + val errorText = remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .background(colorScheme.background), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + value = code.value, + onValueChange = { newText -> code.value = newText }, + label = { Text("Введите код из СМС") }, + textStyle = TextStyle(fontSize = 25.sp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ) + ) + Text( + errorText.value, color = Color.White, fontSize = 25.sp + ) + + val context = LocalContext.current + Button( + modifier = Modifier.padding(16.dp), onClick = { + val packet = SocketManager.packPacket( + OPCode.CHECK_CODE.opcode, JsonObject( + mapOf( + "token" to JsonPrimitive( + intent.getStringExtra("token").toString() + ), + "verifyCode" to JsonPrimitive(code.value), + "authTokenType" to JsonPrimitive("CHECK_CODE") + ) + ) + ) + + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + if ("error" in packet.payload) { + errorText.value = + packet.payload["localizedMessage"].toString() + } else if ("tokenAttrs" in packet.payload) { + if ("REGISTER" in packet.payload["tokenAttrs"]!!.jsonObject) { + val intent = Intent( + context, RegisterActivity::class.java + ) + + val token = + packet.payload["tokenAttrs"]!!.jsonObject["REGISTER"]!!.jsonObject["token"]!!.jsonPrimitive.content + + intent.putExtra("token", token) + + startActivity(intent) + + finish() + } else if ("passwordChallenge" in packet.payload) { + val intent = Intent( + context, PasswordCheckActivity::class.java + ) + val trackId = + packet.payload["passwordChallenge"]?.jsonObject["trackId"]?.jsonPrimitive?.content + val hint = + packet.payload["passwordChallenge"]?.jsonObject["hint"]?.jsonPrimitive?.content + val email = + packet.payload["passwordChallenge"]?.jsonObject["email"]?.jsonPrimitive?.content + + + intent.putExtra("trackId", trackId) + intent.putExtra("hint", hint) + intent.putExtra("email", email) + + context.startActivity(intent) + + finish() + } else { + val intent = Intent( + context, ChatListActivity::class.java + ) + runBlocking { + dataStore.edit { settings -> + // Nice sandwich lol + val token = + packet.payload["tokenAttrs"]!!.jsonObject["LOGIN"]!!.jsonObject["token"]!!.jsonPrimitive.content + settings[stringPreferencesKey("token")] = + token + AccountManager.token = token + } + } + + GlobalScope.launch { + SocketManager.loginToAccount(context) + } + + context.startActivity(intent) + + finish() + } + } + } + } + ) + } + }) { + Text("Войти", fontSize = 25.sp) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/ImageViewerActivity.kt b/app/src/main/java/com/sffteam/voidclient/ImageViewerActivity.kt new file mode 100644 index 0000000..26b4f14 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ImageViewerActivity.kt @@ -0,0 +1,247 @@ +package com.sffteam.voidclient + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Text +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.launch +import androidx.core.view.WindowCompat +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.padding +import androidx.core.view.WindowInsetsCompat + +class ImageViewerActivity : ComponentActivity() { + @SuppressLint("CoroutineCreationDuringComposition") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val isSingleImage = intent.getBooleanExtra("isSingleImage", false) + WindowCompat.setDecorFitsSystemWindows(window, false) + var pickedPhoto = "" + var chatId = 0L + var url = "" + var scrolled = false + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + + if (isSingleImage) { + url = intent.getStringExtra("image").toString() + } else { + chatId = intent.getLongExtra("chatId", 0L) + pickedPhoto = intent.getStringExtra("pickedPhoto").toString() + } + + setContent { + var expanded by remember { mutableStateOf(false) } + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + var images: MutableList> = mutableListOf() + var pagerState = rememberPagerState { images.size } + + var isTopBar by remember { mutableStateOf(true) } + val interactionSource = remember { MutableInteractionSource() } + + if (isTopBar) { + windowInsetsController.show(WindowInsetsCompat.Type.statusBars()) + } else { + windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()) + } + if (!isSingleImage) { + val chats by ChatManager.chatsList.collectAsState() + val viewedChat = chats[chatId] + + for (message in viewedChat!!.messages.toList().sortedByDescending { it.second.sendTime }) { + if (message.second.attaches?.jsonArray?.isNotEmpty() == true) { + println("value $message") + for (attach in message.second.attaches!!.jsonArray) { + if (attach.jsonObject["_type"]?.jsonPrimitive?.content == "PHOTO") { + println(attach.jsonObject["baseUrl"]!!.jsonPrimitive.content) + images.add(attach.jsonObject["baseUrl"]!!.jsonPrimitive.content to false) + + val photoToken = attach.jsonObject["photoToken"]!!.jsonPrimitive.content + + // i think its cringe code :( + if (pickedPhoto == photoToken && !scrolled) { + coroutineScope.launch { + for (img in 0..images.size) { + if (images[img].first == attach.jsonObject["baseUrl"]!!.jsonPrimitive.content && !images[img].second) { + pagerState.scrollToPage(img) + scrolled = true + break + } + } + } + } + } + } + } + + if (message.second.link.msgForLink?.attaches is JsonArray + && message.second.link.msgForLink?.attaches!!.jsonArray.isNotEmpty()) { + println("link $message") + + for (attach in message.second.link.msgForLink.attaches!!.jsonArray) { + if (attach.jsonObject["_type"]?.jsonPrimitive?.content == "PHOTO") { + println(attach.jsonObject["baseUrl"]!!.jsonPrimitive.content) + images.add(attach.jsonObject["baseUrl"]!!.jsonPrimitive.content to true) + + // lol its works + val photoToken = attach.jsonObject["photoToken"]!!.jsonPrimitive.content + + // i think its cringe code :( + if (pickedPhoto == photoToken && !scrolled) { + coroutineScope.launch { + for (img in 0..images.size) { + if (images[img].first == attach.jsonObject["baseUrl"]!!.jsonPrimitive.content && images[img].second) { + pagerState.scrollToPage(img) + scrolled = true + + break + } + } + } + } + } + } + } + } + } + + AppTheme() { + Column (modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .clickable(indication = null, interactionSource = interactionSource) { + isTopBar = !isTopBar + } + ) { + if (isSingleImage) { + AsyncImage( + url, + "", + modifier = Modifier + .fillMaxSize() + .align(Alignment.CenterHorizontally), + contentScale = ContentScale.Fit, + alignment = Alignment.Center + ) + } else { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + ) { page -> + AsyncImage( + images[page].first, + "", + modifier = Modifier + .fillMaxSize() + .align(Alignment.CenterHorizontally), + contentScale = ContentScale.Fit, + alignment = Alignment.Center + ) + } + + } + } + AnimatedVisibility(visible = isTopBar, enter = fadeIn(), exit = fadeOut()) { + Box(modifier = Modifier.background(Color.Black.copy( + 0.6f + ))) { + Column(modifier = Modifier. padding(top = 30.dp, start = 8.dp, end = 8.dp)) { + Row() { + IconButton({ + finish() + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + "", + modifier = Modifier.size(25.dp), + tint = colorScheme.primary + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + IconButton({ + expanded = true + }) { + Icon( + Icons.Filled.MoreVert, + "", + modifier = Modifier.size(25.dp), + tint = colorScheme.primary + ) + + DropdownMenu( + expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.background(colorScheme.secondaryContainer) + ) { + DropdownMenuItem( + text = { Text(text = "Сохранить изображение", color = colorScheme.onSecondaryContainer)}, + onClick = { + val filename = "IMG_${System.currentTimeMillis()}.png" + } + ) + } + } + } + + if (!isSingleImage) { + Text("${pagerState.currentPage + 1} из ${images.size}", + modifier = Modifier.align(Alignment.CenterHorizontally), + color = colorScheme.onSecondaryContainer + ) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/MainActivity.kt b/app/src/main/java/com/sffteam/voidclient/MainActivity.kt new file mode 100644 index 0000000..eafb2c3 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/MainActivity.kt @@ -0,0 +1,256 @@ +package com.sffteam.voidclient + +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.graphics.drawable.toBitmap +import androidx.datastore.preferences.core.stringPreferencesKey +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive + +class MainActivity : ComponentActivity() { + @OptIn( + DelicateCoroutinesApi::class, + ExperimentalMaterial3WindowSizeClassApi::class, + ExperimentalMaterial3Api::class + ) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + // Must be runBlocking because we need to wait for token check + runBlocking { + val exampleData = dataStore.data.first() + AccountManager.token = exampleData[stringPreferencesKey("token")].toString() + } + + val context = this + + val codes = mapOf( + "Россия" to "+7", "Беларусь" to "+375" + ) + GlobalScope.launch { + withContext(Dispatchers.IO) { + SocketManager.connect(context) + } + } + + if (AccountManager.token != "null") { + val intent = Intent(this, ChatListActivity::class.java) + + this.startActivity(intent) + finish() + } + + setContent { + AppTheme { + val phone = remember { mutableStateOf("") } + val errorText = remember { mutableStateOf("") } + var selectedCodeStr = remember { mutableStateOf("Россия") } + var selectedCode = remember { mutableStateOf("+7") } + Utils.windowSize = calculateWindowSizeClass(this) + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .background(colorScheme.background), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val packageManager = context.packageManager + val appIconDrawable: Drawable = + packageManager.getApplicationIcon("com.sffteam.voidclient") + var expanded by remember { mutableStateOf(false) } + + Image( + appIconDrawable.toBitmap(config = Bitmap.Config.ARGB_8888).asImageBitmap(), + contentDescription = "Image", + modifier = Modifier + .size(120.dp) + .padding(8.dp) + .clip(RoundedCornerShape(2.dp)) + ) + + Text( + "Добро пожаловать в Void Client!", + fontSize = 25.sp, + textAlign = TextAlign.Center, + color = colorScheme.primary, + modifier = Modifier.padding(bottom = 2.dp) + ) + + Text( + "Введите свой номер телефона, чтобы войти или зарегистрироваться", + fontSize = 18.sp, + textAlign = TextAlign.Center, + color = colorScheme.primary, + modifier = Modifier.padding(bottom = 10.dp) + ) + + if (errorText.value.isNotEmpty()) { + Text( + "Ошибка: ${errorText.value}", + fontSize = 18.sp, + color = colorScheme.primary, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.heightIn(60.dp) + ) { + // TODO: Rewrite + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = Modifier.width(180.dp) + ) { + OutlinedTextField( + value = selectedCodeStr.value + " (${selectedCode.value})", + onValueChange = {}, + readOnly = true, + maxLines = 1, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier + .menuAnchor() + .heightIn(max = 60.dp) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + codes.toList().forEachIndexed { index, option -> + DropdownMenuItem( + text = { + Text( + option.first, modifier = Modifier.fillMaxSize() + ) + }, + onClick = { + selectedCodeStr.value = option.first + selectedCode.value = option.second + expanded = false + }, + ) + } + } + + } + + OutlinedTextField( + value = phone.value, + onValueChange = { newText -> + phone.value = newText + }, + label = { Text("Номер телефона") }, + textStyle = TextStyle(fontSize = 25.sp), + modifier = Modifier + .width(200.dp) + .height(60.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ) + ) + } + + val context = LocalContext.current + Button( + modifier = Modifier.padding(16.dp), onClick = { + val packet = SocketManager.packPacket( + OPCode.START_AUTH.opcode, JsonObject( + mapOf( + "phone" to JsonPrimitive(selectedCode.value + phone.value), + "type" to JsonPrimitive("START_AUTH"), + "language" to JsonPrimitive("ru") + ) + ) + ) + + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + if ("error" in packet.payload) { + errorText.value = + packet.payload["localizedMessage"]?.jsonPrimitive?.content!! + } else if ("token" in packet.payload) { + val intent = + Intent(context, CodeActivity::class.java) + + println("token " + packet.payload["token"]) + + intent.putExtra( + "token", + packet.payload["token"]!!.jsonPrimitive.content + ) + context.startActivity(intent) + } else { + println("wtf") + } + } + }) + } + }) { + Text("Продолжить", fontSize = 25.sp) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/PasswordCheckActivity.kt b/app/src/main/java/com/sffteam/voidclient/PasswordCheckActivity.kt new file mode 100644 index 0000000..3d24409 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/PasswordCheckActivity.kt @@ -0,0 +1,116 @@ +package com.sffteam.voidclient + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.sp +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +class PasswordCheckActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_password_check2) + + val trackId = intent.getStringExtra("trackId") + + setContent { + val password = remember { mutableStateOf("") } + val errorText = remember { mutableStateOf("") } + + val coroutineScope = rememberCoroutineScope() + AppTheme { + val context = LocalContext.current + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + value = password.value, + onValueChange = { newText -> password.value = newText }, + label = { Text("Введите облачный пароль") }, + textStyle = TextStyle(fontSize = 25.sp), + ) + Text( + errorText.value, color = Color.White, fontSize = 25.sp + ) + + Button(onClick = { + val packet = SocketManager.packPacket( + OPCode.PASSWORD_CHECK.opcode, JsonObject( + mapOf( + "password" to JsonPrimitive(password.value), + "trackId" to JsonPrimitive(trackId) + ) + ) + ) + + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + if (packet.payload.containsKey("error")) { + errorText.value = + packet.payload["message"]!!.jsonPrimitive.content + } else { + val intent = Intent( + context, ChatListActivity::class.java + ) + runBlocking { + dataStore.edit { settings -> + // Nice sandwich lol + val token = + packet.payload["tokenAttrs"]!!.jsonObject["LOGIN"]!!.jsonObject["token"]!!.jsonPrimitive.content + settings[stringPreferencesKey("token")] = token + AccountManager.token = token + } + } + + GlobalScope.launch { + SocketManager.loginToAccount(context) + } + + context.startActivity(intent) + + finish() + } + } + }) + } + }) { + Text("Войти") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/ProfileViewActivity.kt b/app/src/main/java/com/sffteam/voidclient/ProfileViewActivity.kt new file mode 100644 index 0000000..c10476a --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ProfileViewActivity.kt @@ -0,0 +1,319 @@ +package com.sffteam.voidclient + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.filled.Block +import androidx.compose.material.icons.filled.ChatBubble +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Text +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import java.util.Locale.getDefault + +class ProfileViewActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val userId = intent.getLongExtra("userId", 0L) + + setContent { + AppTheme() { + val users by UserManager.usersList.collectAsState() + + val currentUser = users[userId] + val avatarUrl = currentUser?.avatarUrl + val description = currentUser?.description + + val coroutineScope = rememberCoroutineScope() + val fullName = currentUser?.firstName + if (currentUser?.lastName?.isNotEmpty() == true) { + " " + currentUser.lastName + } else { + "" + } + + val context = LocalContext.current + + var userMap : MutableMap = mutableMapOf( + ) + + if (description?.isNotEmpty() == true) { + userMap["О себе"] = description + } + + userMap["ID"] = userId.toString() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(colorScheme.background) + .windowInsetsPadding(WindowInsets.statusBars) + ) { + item { + IconButton({ + finish() + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + "", + modifier = Modifier.size(25.dp), + tint = colorScheme.primary + ) + } + } + + item { + Column(modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically) + ) { + if (avatarUrl?.isNotEmpty() == true) { + AsyncImage( + avatarUrl, + "", + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(110.dp) + .clip(CircleShape) + .clickable { + val intent = Intent(context, ImageViewerActivity::class.java) + + intent.putExtra("isSingleImage", true) + intent.putExtra("image", avatarUrl) + + context.startActivity(intent) + }, + contentScale = ContentScale.FillBounds + ) + } else { + val initial = + fullName?.split(" ")?.mapNotNull { it.firstOrNull() } + ?.take(2)?.joinToString("")?.uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(100.dp) + .height(100.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(fullName).first, + Utils.getColorForAvatar(fullName).second + ) + ) + ) + .align(Alignment.CenterHorizontally), + ) { + Text( + text = initial.toString(), + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 40.sp + ) + } + } + + Text( + fullName, + fontSize = 24.sp, + modifier = Modifier.align(Alignment.CenterHorizontally).padding(start = 8.dp, end = 8.dp), + color = colorScheme.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + item { + Row(modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) + ) { + Button(onClick = { + val chatId = AccountManager.accountID xor userId + + val packet = SocketManager.packPacket( + OPCode.CHAT_INFO.opcode, JsonObject( + mapOf( + "chatIds" to JsonArray(listOf(Json.encodeToJsonElement(Long.serializer(), chatId))), + ) + ) + ) + val intent = Intent(context, ChatActivity::class.java) + + try { + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet) + GlobalScope.launch { + if (packet.payload is JsonObject) ChatManager.processChats( + packet.payload["chats"]!!.jsonArray + ) + } + } + ) + } + } catch (e : Exception) { + println(e) + } + + + intent.putExtra("chatID", chatId) + intent.putExtra("chatTitle", fullName); + intent.putExtra("chatType", "DIALOG") + intent.putExtra("chatIcon", avatarUrl); + + context.startActivity(intent) + }, + modifier = Modifier.padding(4.dp).width(160.dp), + colors = ButtonColors( + containerColor = colorScheme.primaryContainer, + contentColor = colorScheme.onPrimaryContainer, + disabledContainerColor = colorScheme.primaryContainer, + disabledContentColor = colorScheme.onPrimaryContainer, + ) + ) { + Column { + Icon( + Icons.Filled.ChatBubble, + "", + modifier = Modifier.size(25.dp).align(Alignment.CenterHorizontally) + ) + Text("Чат", + fontSize = 18.sp, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + } + + Button(onClick = { + }, + modifier = Modifier.padding(4.dp).width(160.dp), + colors = ButtonColors( + containerColor = colorScheme.primaryContainer, + contentColor = colorScheme.onPrimaryContainer, + disabledContainerColor = colorScheme.primaryContainer, + disabledContentColor = colorScheme.onPrimaryContainer, + ) + ) { + Column() { + Icon( + Icons.Filled.Block, + "", + modifier = Modifier.size(25.dp).align(Alignment.CenterHorizontally) + ) + Text("Заблокировать", + fontSize = 18.sp, + modifier = Modifier.align(Alignment.CenterHorizontally), + maxLines = 1 + ) + } + } +// + // For future updates +// Button(onClick = { +// }, +// modifier = Modifier.padding(4.dp).widthIn(min = 100.dp), +// colors = ButtonColors( +// containerColor = colorScheme.primaryContainer, +// contentColor = colorScheme.onPrimaryContainer, +// disabledContainerColor = colorScheme.primaryContainer, +// disabledContentColor = colorScheme.onPrimaryContainer, +// ) +// ) { +// Column() { +// Icon( +// Icons.Filled.Folder, +// "", +// modifier = Modifier.size(25.dp).align(Alignment.CenterHorizontally) +// ) +// Text("Добавить в папку", +// fontSize = 18.sp, +// modifier = Modifier.align(Alignment.CenterHorizontally), +// maxLines = 1 +// ) +// } +// } + } + } + + item { + Box(modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .background(colorScheme.secondaryContainer, RoundedCornerShape(16.dp)) + ) { + Column(modifier = Modifier.padding(8.dp)) { + for (setting in userMap.toList()) { + + Text(setting.first, modifier = Modifier.alpha(0.7f), + color = colorScheme.onSecondaryContainer, fontSize = 16.sp) + + Text(setting.second, + color = colorScheme.onSecondaryContainer, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 4.dp) + ) + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/RegisterActivity.kt b/app/src/main/java/com/sffteam/voidclient/RegisterActivity.kt new file mode 100644 index 0000000..0591b14 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/RegisterActivity.kt @@ -0,0 +1,134 @@ +package com.sffteam.voidclient + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive + +class RegisterActivity : ComponentActivity() { + @OptIn(DelicateCoroutinesApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val token = intent.getStringExtra("token") + + setContent { + AppTheme { + val firstName = remember { mutableStateOf("") } + val lastName = remember { mutableStateOf("") } + val errorText = remember { mutableStateOf("") } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + value = firstName.value, + onValueChange = { newText -> firstName.value = newText }, + label = { Text("Имя") }, + textStyle = TextStyle(fontSize = 25.sp), + modifier = Modifier.padding(bottom = 15.dp) + ) + + OutlinedTextField( + value = lastName.value, + onValueChange = { newText -> lastName.value = newText }, + label = { Text("Фамилия (необязательно)") }, + textStyle = TextStyle(fontSize = 25.sp), + ) + + Text( + errorText.value, color = Color.White, fontSize = 25.sp + ) + val context = LocalContext.current + + Button(onClick = { + println("fff${firstName.value}fff") + + if (firstName.value.isEmpty()) { + errorText.value = "Имя не может быть пустым!" + } else { + val payload = mutableMapOf( + "firstName" to JsonPrimitive(firstName.value), + ) + if (lastName.value.isNotEmpty()) { + payload["lastName"] = JsonPrimitive(lastName.value) + } + + payload["tokenType"] = JsonPrimitive("REGISTER") + payload["token"] = JsonPrimitive(token) + + val packet = SocketManager.packPacket(23, JsonObject(payload)) + + GlobalScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + if ("error" in packet.payload) { + errorText.value = + packet.payload["localizedMessage"]!!.jsonPrimitive.content + } else if ("token" in packet.payload) { + val intent = + Intent(context, ChatListActivity::class.java) + + runBlocking { + dataStore.edit { settings -> + // Nice sandwich lol + val tokenSettings = + packet.payload["token"]!!.jsonPrimitive.content + settings[stringPreferencesKey("token")] = + tokenSettings + AccountManager.token = tokenSettings + } + } + + GlobalScope.launch { + SocketManager.loginToAccount(context) + } + + context.startActivity(intent) + + finish() + } else { + println("wtf") + } + } + }) + } + } + }) { + Text("Продолжить", fontSize = 25.sp) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/SocketManager.kt b/app/src/main/java/com/sffteam/voidclient/SocketManager.kt new file mode 100644 index 0000000..ee94143 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/SocketManager.kt @@ -0,0 +1,490 @@ +package com.sffteam.voidclient + +import android.content.Context +import android.content.Intent +import android.icu.util.TimeZone +import android.os.Build +import android.provider.Settings +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.fasterxml.jackson.databind.ObjectMapper +import io.ktor.network.selector.SelectorManager +import io.ktor.network.sockets.Socket +import io.ktor.network.sockets.aSocket +import io.ktor.network.sockets.openReadChannel +import io.ktor.network.sockets.openWriteChannel +import io.ktor.network.tls.tls +import io.ktor.utils.io.cancel +import io.ktor.utils.io.readAvailable +import io.ktor.utils.io.writeFully +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import net.jpountz.lz4.LZ4Factory +import net.jpountz.lz4.LZ4FastDecompressor +import org.msgpack.jackson.dataformat.MessagePackFactory +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.Locale +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.time.Duration.Companion.seconds + +const val host = "api.oneme.ru" +const val port = 443 +const val API_VERSION = 10 // lol +var Seq = 1 + +enum class OPCode(val opcode: Int) { + PING(1), + START(6), // Using that on open socket + CHANGE_PROFILE(16), + START_AUTH(17), + CHECK_CODE(18), // Also can be LOGIN packet from server or WRONG_CODE from server + PROFILE_INFO(19), // Server returns profile info with that opcode + LOGOUT(20), + SETTINGS_CHANGE(22), + NEW_STICKER_SETS(26), // Idk, implement it later + SYNC_EMOJI(27), // Also syncs ANIMOJI, REACTIONS, STICKERS, FAVORITE_STICKER + ANIMOJI(28), // Idk + CONTACTS_INFO(32), // Returns info about ids that your sent (if you sent ids that not your contacts, server return you just a empty array) + LAST_SEEN(35), // Used for obtain last seen of contacts + CHAT_INFO(48), + CHAT_MESSAGES(49), + EDIT_CHAT_INFO(55), + JOIN_CHAT(57), + LEAVE_CHAT(58), + SEND_MESSAGE(64), + DELETE_MESSAGE(66), + EDIT_MESSAGE(67), + CHAT_SUBSCRIBE(75), // Idk + WHO_CAN_SEE(76), // Used for disable or enable status online + EDIT_ADMIN_PERMISSION(77), + HISTORY(79), // Idk + UPLOAD_IMAGE(80), + GET_FILE(88), + SESSIONS(96), // Used for obtain all sessions for account + SETTINGS_UPDATE(134), + SESSIONS_EXIT(97), + PASSWORD_CHECK(115), + SYNC_FOLDER(272), + QR_CODE(290) +} + +@Serializable +data class Packet( + @SerialName("ver") val ver: Int = API_VERSION, + @SerialName("cmd") val cmd: Int = 0, + @SerialName("seq") val seq: Int = Seq, + @SerialName("opcode") val opcode: Int, + @SerialName("payload") @Contextual val payload: JsonElement, +) + +data class PacketCallback(val seq: Int, val callback: (Packet) -> Unit) + +fun Short.toByteArrayBigEndian(): ByteArray { + return ByteBuffer.allocate(Short.SIZE_BYTES).putShort(this).array() +} + +fun Int.toByteArrayBigEndian(): ByteArray { + return byteArrayOf( + (this ushr 24).toByte(), (this ushr 16).toByte(), (this ushr 8).toByte(), this.toByte() + ) +} + +fun messagePackToJson(bytes: ByteArray): String { + val msgpackMapper = ObjectMapper(MessagePackFactory()) + val jsonMapper = ObjectMapper() + + val node = msgpackMapper.readTree(bytes) + return jsonMapper.writeValueAsString(node) +} + +fun jsonToMessagePack(json: String): ByteArray { + val jsonMapper = ObjectMapper() + val msgPackMapper = ObjectMapper(MessagePackFactory()) + + val tree = jsonMapper.readTree(json) + return msgPackMapper.writeValueAsBytes(tree) +} + +object SocketManager { + private val selectorManager = SelectorManager(Dispatchers.IO) + private lateinit var socket: Socket + + private val subscribers = CopyOnWriteArrayList<(String) -> Unit>() + + private var packetCallbacks = mutableListOf() + + fun packPacket(opcode: Int, payload: JsonElement): ByteArray { + // Thanks to https://github.com/ink-developer/PyMax/blob/main/src/pymax/mixins/socket.py#L75 again :D + val apiVer = API_VERSION.toByte() + val cmd = 0.toByte() + val seq = Seq.toShort().toByteArrayBigEndian() + val opcode = opcode.toShort().toByteArrayBigEndian() + println("string ${payload.toString()}") + val payload = jsonToMessagePack(payload.toString()) + val payloadLen = payload.size and 0xFFFFFF + + return byteArrayOf( + apiVer, cmd, *seq, *opcode, *payloadLen.toByteArrayBigEndian(), *payload + ) + } + + fun unpackPacket(data: ByteArray): Packet { + // Thanks to https://github.com/ink-developer/PyMax/blob/main/src/pymax/mixins/socket.py#L42 + val factory = LZ4Factory.fastestInstance() + val decompressor: LZ4FastDecompressor = factory.fastDecompressor() + + val apiVer = data[0].toInt() and 0xFF + val cmd = data[1].toInt() and 0xFF + val seqSigned = ByteBuffer.wrap(data, 2, 2).order(ByteOrder.BIG_ENDIAN).short + val seq = seqSigned.toInt() and 0xFFFF + + val opcodeSigned = ByteBuffer.wrap(data, 4, 2).order(ByteOrder.BIG_ENDIAN).short + val opcode = opcodeSigned.toInt() and 0xFFFF + + val packedLen = + ByteBuffer.wrap(data, 6, 4).order(ByteOrder.BIG_ENDIAN).int.toLong() and 0xFFFFFFFFL + + val compFlag = (packedLen shr 24).toInt() + val payloadLength = (packedLen and 0xFFFFFF).toInt() + + val payloadBytes = data.sliceArray(10 until (10 + payloadLength)) + var payload = "" + + if (payloadBytes.isNotEmpty()) { + if (compFlag != 0) { + var decompressedBytes = ByteArray(131072) + println("test1") + try { + decompressor.decompress(payloadBytes, decompressedBytes) + } catch (e: Exception) { + println("decomp err ${e}") + } + + println("test2") + + try { + payload = messagePackToJson(decompressedBytes) + } catch (e: Exception) { + println(e) + } + } else { + payload = messagePackToJson(payloadBytes) + } + } + + println("payload! ${payload}") + var jsonPayload = JsonObject(emptyMap()) + + if (payload.isNotEmpty()) { + jsonPayload = Json.decodeFromString(payload) + } + return Packet( + apiVer, cmd, seq, opcode, jsonPayload + ) + } + + suspend fun sendStartPacket(context: Context): Boolean { + sendPacket( + packPacket( + OPCode.START.opcode, JsonObject( + mapOf( + "clientSessionId" to JsonPrimitive(192L), "userAgent" to JsonObject( + mapOf( + "deviceType" to JsonPrimitive("ANDROID"), + "appVersion" to JsonPrimitive("25.21.0"), + "osVersion" to JsonPrimitive("Android ${Build.VERSION.RELEASE}"), + "timezone" to JsonPrimitive(TimeZone.getDefault().id), + "screen" to JsonPrimitive("382dpi 382dpi 1080x2243"), + "pushDeviceType" to JsonPrimitive("GCM"), + "locale" to JsonPrimitive("ru"), + "buildNumber" to JsonPrimitive(6420), + "deviceName" to JsonPrimitive(Build.MANUFACTURER + " " + Build.MODEL), + "deviceLocale" to JsonPrimitive(Locale.getDefault().language.toString()), + ) + ), "deviceId" to JsonPrimitive( + Settings.Secure.getString( + context.contentResolver, Settings.Secure.ANDROID_ID + ) + ) + ) + ) + ), { packet -> + println("response") + println(packet.payload) + }) + + return true + } + + suspend fun connect(context: Context) = coroutineScope { + println("trying to connect") + + while (true) { + try { + socket = aSocket(selectorManager).tcp().connect(host, port) + .tls(coroutineContext = currentCoroutineContext()) + + val result = sendStartPacket(context) + + if (result) { + if (AccountManager.token != "null") { + loginToAccount(context) + AccountManager.logined = true + } + async { + sendPing() + } + + getPackets() + } + } catch (e: Exception) { + println(e) + } + + delay(50) + } + } + + suspend fun loginToAccount(context: Context) = coroutineScope { + val packet = packPacket( + OPCode.PROFILE_INFO.opcode, JsonObject( + mapOf( + "interactive" to JsonPrimitive(true), + "token" to JsonPrimitive(AccountManager.token), + "chatsCount" to JsonPrimitive(40), + "chatsSync" to JsonPrimitive(0), + "contactsSync" to JsonPrimitive(0), + "presenceSync" to JsonPrimitive(0), + "draftsSync" to JsonPrimitive(0), + ) + ) + ) + + sendPacket( + packet, { packet -> + if (packet.payload.jsonObject.containsKey("error")) { + val intent: Intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + AccountManager.token = "null" + + println(AccountManager.token) + + runBlocking { + try { + context.dataStore.edit { settings -> + settings[stringPreferencesKey("token")] = "null" + } + } catch (e: Exception) { + println(e) + } + } + context.startActivity(intent) + } else { + println(packet) + try { + AccountManager.accountID = + packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["id"]!!.jsonPrimitive.long + AccountManager.phone = + packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["phone"]!!.jsonPrimitive.content + + UserManager.processMyProfile(packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject) + + AccountManager.processSettings(packet.payload.jsonObject["config"]!!.jsonObject["user"]!!.jsonObject) + val packet = SocketManager.packPacket( + OPCode.CONTACTS_INFO.opcode, JsonObject( + mapOf( + "contactIds" to JsonArray(listOf(JsonPrimitive(AccountManager.accountID))), + ) + ) + ) + + GlobalScope.launch { + sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + GlobalScope.launch { + UserManager.processUsers(packet.payload["contacts"]!!.jsonArray) + } + } + } + ) + } + } catch (e: Exception) { + println(e) + } + try { + val test = packet.payload.jsonObject["chats"]!!.jsonArray + println(test) + } catch (e: Exception) { + println(e) + } + println() + + GlobalScope.launch { + ChatManager.processChats(packet.payload.jsonObject["chats"]!!.jsonArray) + } + } + }) + } + + suspend fun sendPacket(packet: ByteArray, callback: (Packet) -> Unit) { + val sendChannel = socket.openWriteChannel(autoFlush = true) + + println(unpackPacket(packet)) + sendChannel.writeFully(packet) + sendChannel.flush() + + packetCallbacks.add(PacketCallback(Seq, callback)) + + Seq += 1 + } + + suspend fun getPackets() { + val receiveChannel = socket.openReadChannel() + try { + var entirePacket = ByteArray(131072) + var pos = 0 + while (socket.isActive) { + val buffer = ByteArray(8192) + val bytesRead = receiveChannel.readAvailable(buffer, 0, 8192) + + if (bytesRead == -1) { + break + } + + println(bytesRead) + println(buffer.size) + + if (bytesRead > 0) { + if (bytesRead == 8192) { // tmp solution + entirePacket = buffer.copyInto(entirePacket, pos) + pos += 8192 + continue + } + entirePacket = buffer.copyInto(entirePacket, pos, 0, bytesRead) + pos += bytesRead + println("Total packet length: ${pos}") + val packet = unpackPacket(entirePacket.sliceArray(0.. + if (cb.seq == packet.seq) { + cb.callback(packet) + SocketManager.packetCallbacks.removeAt(i) + return@loop + } + } + } + + println(packet) + println() + } + } + } catch (e: Exception) { + println(e) + } finally { + receiveChannel.cancel() + socket.close() + } + } + + suspend fun sendPing() { + val packet = packPacket( + OPCode.PING.opcode, JsonObject( + mapOf( + "interactive" to JsonPrimitive(false), + ) + ) + ) + + while (true) { + delay(20.seconds) + sendPacket(packet, {}) + println("ping!") + } + } +} diff --git a/app/src/main/java/com/sffteam/voidclient/UserManager.kt b/app/src/main/java/com/sffteam/voidclient/UserManager.kt new file mode 100644 index 0000000..8a3f3d1 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/UserManager.kt @@ -0,0 +1,204 @@ +package com.sffteam.voidclient + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long + +data class User( + val avatarUrl: String, val firstName: String, val lastName: String, val lastSeen: Long, val description : String +) + +object UserManager { + private val _usersList = MutableStateFlow>(emptyMap()) + var usersList = _usersList.asStateFlow() + + fun clearUsersList() { + _usersList.update { + emptyMap() + } + } + + fun processMyProfile(profile : JsonObject) { + println("myprofile $profile") + var userID = 0L + try { + userID = profile.jsonObject["id"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + println(userID) + + try { + var avatarUrl = "" + var firstName = "" + var lastName = "" + var lastSeen = 0L + var desc = "" + try { + avatarUrl = profile.jsonObject["baseUrl"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + println("0msg") + } + + try { + firstName = + profile.jsonObject["names"]!!.jsonArray[0].jsonObject["firstName"]?.jsonPrimitive!!.content + } catch (e: Exception) { + println("1msg") + println(e) + } + + try { + lastName = + profile.jsonObject["names"]!!.jsonArray[0].jsonObject["lastName"]?.jsonPrimitive!!.content + } catch (e: Exception) { + println("5msg") + println(e) + } + + val currentMap = mapOf( + userID to User( + avatarUrl, firstName, lastName, 0L, desc + ) + ) + + _usersList.update { + it.toMap() + currentMap + } + + } catch (e: Exception) { + println(e) + } + } + + // TODO: Process users presence + fun processPresence(presences: JsonArray) { + for (i in presences.jsonObject.toList()) { + val prs = i.second.jsonObject["seen"]?.jsonPrimitive?.long + + _usersList.update { oldMap -> + oldMap + (i.first.toLong() to User( + oldMap[i.first.toLong()]?.avatarUrl ?: "", + oldMap[i.first.toLong()]?.firstName ?: "", + oldMap[i.first.toLong()]?.lastName ?: "", + i.second.jsonObject["seen"]?.jsonPrimitive?.long ?: 0L, + oldMap[i.first.toLong()]?.description ?: "" + )) + } + } + } + + fun processUsers(contacts: JsonArray) { + println("cool users $contacts") + for (i in contacts) { + var userID: Long = 0 + + try { + userID = i.jsonObject["id"]!!.jsonPrimitive.long + } catch (e: Exception) { + println(e) + } + + println(userID) + + try { + var avatarUrl = "" + var firstName = "" + var lastName = "" + var lastSeen = 0L + var desc = "" + + try { + avatarUrl = i.jsonObject["baseUrl"]!!.jsonPrimitive.content + } catch (e: Exception) { + println(e) + println("0msg") + } + + try { + firstName = + i.jsonObject["names"]!!.jsonArray[0].jsonObject["firstName"]?.jsonPrimitive!!.content + } catch (e: Exception) { + println("1msg") + println(e) + } + + try { + lastName = + i.jsonObject["names"]!!.jsonArray[0].jsonObject["lastName"]?.jsonPrimitive!!.content + } catch (e: Exception) { + println("5msg") + println(e) + } + + try { + desc = + i.jsonObject["description"]!!.jsonPrimitive.content + } catch (e: Exception) { + println("5msg") + println(e) + } + + + val currentMap = mapOf( + userID to User( + avatarUrl, firstName, lastName, 0L, desc + ) + ) + + _usersList.update { + it.toMap() + currentMap + } + + } catch (e: Exception) { + println(e) + } + println(_usersList.value.toMap()) + println("processing") + } + } + + fun checkForExisting(user: Long) { + if (!usersList.value.containsKey(user)) { + val packet = SocketManager.packPacket( + OPCode.CONTACTS_INFO.opcode, JsonObject( + mapOf( + "contactIds" to JsonArray( + listOf( + Json.encodeToJsonElement( + Long.serializer(), user + ) + ) + ), + ) + ) + ) + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + println(packet.payload) + if (packet.payload is JsonObject) { + GlobalScope.launch { + if (packet.payload["contacts"]?.jsonArray?.isNotEmpty() == true) { + processUsers(packet.payload["contacts"]!!.jsonArray) + } + } + } + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/Utils.kt b/app/src/main/java/com/sffteam/voidclient/Utils.kt new file mode 100644 index 0000000..350f183 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/Utils.kt @@ -0,0 +1,91 @@ +package com.sffteam.voidclient + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Article +import androidx.compose.material.icons.automirrored.filled.InsertDriveFile +import androidx.compose.material.icons.filled.Article +import androidx.compose.material.icons.filled.AudioFile +import androidx.compose.material.icons.filled.InsertDriveFile +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import java.io.File +import kotlin.math.absoluteValue + +object Utils { + lateinit var windowSize: WindowSizeClass + + val audioExtensions = listOf( + "mp3", "aac", "m4a", "ogg", "oga", "opus", "wma", "amr", "3gp", + "flac", "alac", "ape", "wav", "aiff", "aif", "aifc", "wv", "tta", "tak", "shn", + "dsf", "dff", "dsd", "pcm", "dxp", "pt24", + "mid", "midi", "rmi", "kar", + "mod", "xm", "s3m", "it", "mtm", "umx", "mo3", + "caf", "au", "snd", "ra", "rm", "mka", "weba", "ac3", "eac3", "dts", "m4b", + "voc", "8svx", "cda", "gsm", "mpc", "spx", "la" + ) + + fun getIconForFile(file : String) : ImageVector { + val extension = File(file).extension + + if (extension == "txt") { + return Icons.AutoMirrored.Filled.Article + } + + for (i in audioExtensions) { + if (extension == i) { + return Icons.Filled.AudioFile + } + } + + return Icons.AutoMirrored.Filled.InsertDriveFile + } + fun getSizeFromBytes(bytes : Long) : String { + if (bytes <= 1000) { + return "$bytes B" + } + + if (bytes in 1000..<1000000) { + val kb = bytes / 1000 + + return "$kb KB" + } + + if (bytes > 1000000) { + val mb = bytes / 1000000 + + return "$mb MB" + } + + return "$bytes" + } + fun getColorForAvatar(avatar: String): Pair { + val colors = listOf( + Pair(Color(0xFFFF0026), Color(0xFFFF00BB)), + Pair(Color(0xFFFFC004), Color(0xFFFFE59F)), + Pair(Color(0xFF0A5BC2), Color(0xFF3B8FFF)), + Pair(Color(0xFF04C715), Color(0xFF6AFC78)), + Pair(Color(0xFFA308C4), Color(0xFFE071FC)), + ) + + val index = (avatar.hashCode().absoluteValue) % colors.size + + return colors[index] + } + + fun getColorForNickname(nickName: String): Color { + val colors = listOf( + Color(0xFFFF2B4B), + Color(0xFFF8C324), + Color(0xFFFD903C), + Color(0xFF2196F3), + Color(0xFF2BFF47), + Color(0xFFE139FF), + Color(0xFF41E0D2), + Color(0xFF7826FC), + ) + val index = (nickName.hashCode().absoluteValue) % colors.size + + return colors[index] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/preferences/AboutActivity.kt b/app/src/main/java/com/sffteam/voidclient/preferences/AboutActivity.kt new file mode 100644 index 0000000..feb5b2d --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/preferences/AboutActivity.kt @@ -0,0 +1,244 @@ +package com.sffteam.voidclient.preferences + +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.graphics.drawable.toBitmap +import com.sffteam.voidclient.R +import com.sffteam.voidclient.ui.theme.AppTheme +import sh.calvin.autolinktext.rememberAutoLinkText + +class AboutActivity : ComponentActivity() { + val specialThanks = listOf( + "Kolyah35", + "CITRIM", + "DeL", + "FullHarmony", + "danilka22ah", + "njuyse", + "TeamKomet", + "a555lieva", + "Irishka_Piper", + ) + + val developers = mapOf( + "InviseDivine" to "Разработчик, дизайнер, основатель проекта", + "Jaan" to "Помощь с разработкой SocketManager'а" + ) + + val infoText = "Void Client - самописный клиент для MAX'а с открытым исходным кодом" + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_about) + + setContent { + AppTheme() { + val context = LocalContext.current + + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceContainer, + ), + title = { + Text("О приложении") + }, + navigationIcon = { + IconButton({ finish() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + contentDescription = "Меню" + ) + } + }, + ) + }) { + LazyColumn( + modifier = Modifier + .padding(it) + .fillMaxWidth() + .padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + item { + val packageManager = context.packageManager + val appIconDrawable: Drawable = + packageManager.getApplicationIcon("com.sffteam.voidclient") + + Image( + appIconDrawable.toBitmap(config = Bitmap.Config.ARGB_8888) + .asImageBitmap(), + contentDescription = "Image", modifier = Modifier + .size(100.dp) + .padding(8.dp) + .clip(CircleShape) + ) + } + item { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + .padding(start = 4.dp, top = 4.dp, end = 4.dp) + ) { + Column() { + Text( + "О приложении", + fontSize = 24.sp, + color = colorScheme.primary, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) + + Column() { + Text( + infoText, + fontSize = 22.sp, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) + + Text( + AnnotatedString.rememberAutoLinkText( + "Наши ссылки: \n" + + "Github - https://github.com/InviseDivine/Void-Client \n" + + "Telegram - t.me/max_voidclient", + ), + fontSize = 20.sp, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) + } + } + } + } + item { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + .padding(start = 4.dp, top = 4.dp, end = 4.dp) + ) { + Column() { + Text( + "Разработчики", + fontSize = 24.sp, + color = colorScheme.onSecondaryContainer, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) + + for (dev in developers.toList()) { + DrawDevelopers(dev.first, dev.second) + } + } + } + } + + item { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + .padding(start = 4.dp, top = 4.dp, end = 4.dp) + ) { + Column() { + Text( + "Отдельная благодарность", + fontSize = 24.sp, + color = colorScheme.onSecondaryContainer, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) + + for (people in specialThanks) { + DrawSpecialThanks(people) + } + } + } + } + } + } + } + } + } +} + +@Composable +fun DrawDevelopers(people: String, desc: String) { + // TODO : Avatars + Column() { + Text( + people, + fontSize = 22.sp, + modifier = Modifier + .padding(start = 4.dp, bottom = 2.dp), + color = colorScheme.onSecondaryContainer, + ) + + Text( + desc, + fontSize = 18.sp, + modifier = Modifier + .padding(start = 4.dp, bottom = 4.dp) + .alpha(0.7f), + color = colorScheme.onSecondaryContainer, + ) + } +} + +@Composable +fun DrawSpecialThanks(people: String) { + Text( + people, + fontSize = 20.sp, + modifier = Modifier + .padding(start = 4.dp, bottom = 4.dp) + .alpha(0.8f), + color = colorScheme.onSecondaryContainer, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/preferences/ChatSettingsActivity.kt b/app/src/main/java/com/sffteam/voidclient/preferences/ChatSettingsActivity.kt new file mode 100644 index 0000000..d386888 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/preferences/ChatSettingsActivity.kt @@ -0,0 +1,92 @@ +package com.sffteam.voidclient.preferences + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.sffteam.voidclient.AccountManager +import com.sffteam.voidclient.OPCode +import com.sffteam.voidclient.SocketManager +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray + +class ChatSettingsActivity : AppCompatActivity() { + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + val coroutineScope = rememberCoroutineScope() + if (AccountManager.sessionsList.value.isEmpty()) { + val packet = + SocketManager.packPacket(OPCode.SESSIONS.opcode, JsonObject(emptyMap())) + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + AccountManager.processSession(packet.payload["sessions"]!!.jsonArray) + } + }) + } + } + + AppTheme { + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceContainer, + ), + title = { + Text("Настройки чатов") + }, + navigationIcon = { + IconButton({ finish() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + contentDescription = "Меню" + ) + } + }, + ) + } + ) { + + LazyColumn( + modifier = Modifier + .padding(it) + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, bottom = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item() {} + + item() {} + + item() {} + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/preferences/DevicesActivity.kt b/app/src/main/java/com/sffteam/voidclient/preferences/DevicesActivity.kt new file mode 100644 index 0000000..f530a99 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/preferences/DevicesActivity.kt @@ -0,0 +1,272 @@ +package com.sffteam.voidclient.preferences + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.outlined.Android +import androidx.compose.material.icons.outlined.PhoneIphone +import androidx.compose.material.icons.outlined.Web +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight.Companion.Bold +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.sffteam.voidclient.AccountManager +import com.sffteam.voidclient.OPCode +import com.sffteam.voidclient.Session +import com.sffteam.voidclient.SocketManager +import com.sffteam.voidclient.dataStore +import com.sffteam.voidclient.ui.theme.AppTheme +import io.github.g00fy2.quickie.QRResult +import io.github.g00fy2.quickie.ScanQRCode +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import java.time.Duration +import java.util.Date +import kotlin.time.ExperimentalTime +import kotlin.time.Instant.Companion.fromEpochMilliseconds + +class DevicesActivity : ComponentActivity() { + val scanQrCodeLauncher = registerForActivityResult(ScanQRCode()) { result -> + if (result is QRResult.QRSuccess) { + val packet = SocketManager.packPacket( + OPCode.QR_CODE.opcode, JsonObject( + mapOf( + "qrLink" to JsonPrimitive(result.content.rawValue.toString()) + ) + ) + ) + + runBlocking { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + println(packet.payload) + } + }) + } + } + } + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + val coroutineScope = rememberCoroutineScope() + if (AccountManager.sessionsList.collectAsState().value.isEmpty()) { + val packet = + SocketManager.packPacket(OPCode.SESSIONS.opcode, JsonObject(emptyMap())) + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + AccountManager.processSession(packet.payload["sessions"]!!.jsonArray) + } + }) + } + } + + AppTheme { + val context = LocalContext.current + + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceContainer, + ), + title = { + Text("Устройства") + }, + navigationIcon = { + IconButton({ finish() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + contentDescription = "Меню" + ) + } + }, + ) + }) { + + LazyColumn( + modifier = Modifier + .padding(it) + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, bottom = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item() { + Button(onClick = { + scanQrCodeLauncher.launch(null) + }) { + Text("Войти по QR коду", fontSize = 18.sp) + } + } + + item() { + Button(onClick = { + val packet = SocketManager.packPacket( + OPCode.SESSIONS_EXIT.opcode, + JsonObject(emptyMap()) + ) + + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + println(packet.payload) + try { + runBlocking { + context.dataStore.edit { settings -> + settings[stringPreferencesKey("token")] = + packet.payload["token"]!!.jsonPrimitive.content + SocketManager.loginToAccount(context) + } + } + } catch (e: Exception) { + println(e) + } + } + }) + + } + }) { + Text("Завершить все сеансы", fontSize = 18.sp, color = Color.Red) + } + } + + item() { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + .padding(start = 4.dp, top = 4.dp, end = 4.dp) + ) { + Column() { + val sessions by AccountManager.sessionsList.collectAsState() + + Column(modifier = Modifier) { + Text( + "Активные сеансы", + fontSize = 22.sp, + color = colorScheme.onSecondaryContainer, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) + + for (session in sessions.sortedByDescending { value -> value.time }) { + DrawSessions(session) + } + } + } + } + } + } + } + } + } + } +} + +@OptIn(ExperimentalTime::class) +@Composable +fun DrawSessions(session: Session) { + Box(modifier = Modifier.padding(bottom = 8.dp)) { + Row() { + val lastMessageTime = session.time + val currentTime = Date().time + + val instantLast = fromEpochMilliseconds(lastMessageTime) + + val duration = Duration.ofSeconds(currentTime / 1000 - lastMessageTime / 1000) + + val localDateTime = instantLast.toLocalDateTime(TimeZone.currentSystemDefault()) + + val hours = if (localDateTime.hour < 10) { + "0${localDateTime.hour}" + } else { + localDateTime.hour + } + + val minutes = if (localDateTime.minute < 10) { + "0${localDateTime.minute}" + } else { + localDateTime.minute + } + + val time = if (duration.toHours() < 24) { + "${hours}:${minutes}" + } else { + "${hours}:${minutes} ${localDateTime.date}" + } + + val icon = if (session.client == "MAX WEB") { + Icons.Outlined.Web + } else if (session.client == "MAX Android") { + Icons.Outlined.Android + } else { + // TODO : Change to IOS icon + Icons.Outlined.PhoneIphone + } + Icon( + icon, + "lol", + modifier = Modifier + .size(40.dp) + .align(Alignment.CenterVertically) + .padding(end = 4.dp), tint = colorScheme.onSecondaryContainer + ) + + Column(modifier = Modifier.weight(0.8f)) { + val client = if (session.current) { + session.client + " (Текущая)" + } else { + session.client + } + Text(text = client, fontSize = 18.sp, fontWeight = Bold, color = colorScheme.onSecondaryContainer) + Text(text = session.info, fontSize = 16.sp, modifier = Modifier.alpha(0.7f), color = colorScheme.onSecondaryContainer) + Text(text = session.location, fontSize = 16.sp, modifier = Modifier.alpha(0.7f), color = colorScheme.onSecondaryContainer) + } + + Text(text = time, fontSize = 16.sp, color = colorScheme.onSecondaryContainer) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/preferences/ProfileSettingsActivity.kt b/app/src/main/java/com/sffteam/voidclient/preferences/ProfileSettingsActivity.kt new file mode 100644 index 0000000..74c3381 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/preferences/ProfileSettingsActivity.kt @@ -0,0 +1,433 @@ +package com.sffteam.voidclient.preferences + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.OpenableColumns +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import com.sffteam.voidclient.AccountManager +import com.sffteam.voidclient.ChatManager +import com.sffteam.voidclient.MainActivity +import com.sffteam.voidclient.OPCode +import com.sffteam.voidclient.SocketManager +import com.sffteam.voidclient.UserManager +import com.sffteam.voidclient.Utils +import com.sffteam.voidclient.ui.theme.AppTheme +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.request +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.headers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import java.util.Locale.getDefault + +class ProfileSettingsActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + AppTheme { + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceContainer, + ), + title = { + Text("Профиль") + }, + navigationIcon = { + IconButton({ finish() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + contentDescription = "Меню" + ) + } + }, + ) + }) { + Column( + modifier = Modifier + .padding(it) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + val users by UserManager.usersList.collectAsState() + val you = users[AccountManager.accountID] + val firstName = remember { mutableStateOf(you!!.firstName) } + val lastName = remember { mutableStateOf(you?.lastName ?: "") } + val desc = remember { mutableStateOf(you?.description) } + val context = LocalContext.current + var selectedImages by remember { + mutableStateOf>(emptyList()) + } + + val singlePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> + println("uris $uri") + selectedImages = listOf(uri) + } + ) + + Box( + modifier = Modifier.clickable { + singlePhotoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + }, contentAlignment = Alignment.Center + ) { + if (you?.avatarUrl?.isNotEmpty() == true) { + AsyncImage( + model = you.avatarUrl, + contentDescription = "ChatIcon", + modifier = Modifier + .width(100.dp) + .height(100.dp) + .clip(CircleShape) + .align(Alignment.Center), + contentScale = ContentScale.Crop, + ) + } else { + val fullName = you?.firstName + you?.lastName + val initial = + fullName.split(" ").mapNotNull { it.firstOrNull() }.take(2) + .joinToString("").uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(100.dp) + .height(100.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(fullName).first, + Utils.getColorForAvatar(fullName).second + ) + ) + ), + + ) { + Text( + text = initial, + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 25.sp + ) + } + } + + Box( + modifier = Modifier + .background( + colorScheme.primaryContainer, + shape = RoundedCornerShape(8.dp) + ) + .align(Alignment.BottomEnd), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Filled.PhotoCamera, + contentDescription = "Меню", + modifier = Modifier + .size(30.dp) + .align(Alignment.Center), + tint = colorScheme.onPrimaryContainer + ) + } + } + OutlinedTextField( + value = firstName.value, + onValueChange = { newText -> + if (newText.length <= 59) { + firstName.value = newText + } + }, + label = { Text("Имя") }, + textStyle = TextStyle(fontSize = 25.sp), + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + + OutlinedTextField( + value = lastName.value, + onValueChange = { newText -> + if (newText.length <= 59) { + lastName.value = newText + } + }, + label = { Text("Фамилия") }, + textStyle = TextStyle(fontSize = 25.sp), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + OutlinedTextField( + value = desc.value.toString(), + onValueChange = { newText -> + if (newText.length <= 400) { + desc.value = newText + } + }, + label = { Text("О себе") }, + textStyle = TextStyle(fontSize = 25.sp), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Button( + onClick = { + if (selectedImages.isNotEmpty()) { + println("mr") + var uploadedImages = mapOf() + + var imageType = "" + var imageName = "" + val cursor = context.contentResolver.query( + selectedImages.last()!!, null, null, null, null + ) + cursor?.use { + if (it.moveToFirst()) { + val nameIndex = + it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + + imageName = it.getString(nameIndex) + } + } + + val packet = SocketManager.packPacket( + OPCode.UPLOAD_IMAGE.opcode, JsonObject( + mapOf( + "count" to JsonPrimitive(1) + ) + ) + ) + val client = HttpClient(CIO) + + runBlocking { + println("pen") + val imageBytes = try { + context.contentResolver.openInputStream(selectedImages.last()!!) + ?.use { inputStream -> + inputStream.readBytes() + } + } catch (e: Exception) { + null + } + + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + runBlocking { + try { + val response: HttpResponse = + client.post(packet.payload["url"]?.jsonPrimitive?.content.toString()) { + method = HttpMethod.Post + + headers { + append( + HttpHeaders.UserAgent, + "OKMessages/25.12.1 (Android 14; oneplus CPH2465; 382dpi 2300x1023)" + ) + append( + HttpHeaders.ContentType, + "application/octet-stream" + ) + append( + HttpHeaders.ContentDisposition, + "attachment; filename=${imageName}" + ) + append( + "X-Uploading-Mode", + "parallel" + ) + append( + "Content-Range", + "bytes 0-${imageBytes!!.size - 1}/${imageBytes.size}" + ) + append( + HttpHeaders.Connection, + "keep-alive" + ) + append( + HttpHeaders.AcceptEncoding, + "gzip" + ) + } + + setBody(imageBytes) + } + + println(response.request.content) + println("Upload response status: ${response.status}") + val content = + Json.parseToJsonElement(response.bodyAsText()) + + uploadedImages = + content.jsonObject["photos"]!!.jsonObject + + print(content) + + + println("is") + var packetJson = mutableMapOf( + "firstName" to JsonPrimitive(firstName.value), + "lastName" to JsonPrimitive(lastName.value), + ) + packetJson["description"] = + JsonPrimitive(desc.value) + + packetJson["avatarType"] = + JsonPrimitive("USER_AVATAR") + packetJson["photoToken"] = JsonPrimitive( + uploadedImages.toList() + .last().second.jsonObject["token"]!!.jsonPrimitive.content + ) + + val packet = SocketManager.packPacket( + OPCode.CHANGE_PROFILE.opcode, + JsonObject(packetJson) + ) + + println("gay") + GlobalScope.launch { + SocketManager.sendPacket( + packet, { packet -> + if (packet.payload is JsonObject) { + AccountManager.accountID = + packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["id"]!!.jsonPrimitive.long + + AccountManager.phone = + packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["phone"]!!.jsonPrimitive.content + } + }) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + client.close() + } + } + } + }) + } + } else { + var packetJson = mutableMapOf( + "firstName" to JsonPrimitive(firstName.value), + "lastName" to JsonPrimitive(lastName.value), + ) + packetJson["description"] = JsonPrimitive(desc.value) + + val packet = SocketManager.packPacket( + OPCode.CHANGE_PROFILE.opcode, JsonObject(packetJson) + ) + + GlobalScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + AccountManager.accountID = + packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["id"]!!.jsonPrimitive.long + + AccountManager.phone = + packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["phone"]!!.jsonPrimitive.content + } + }) + } + } + }) { + Text("Сохранить", fontSize = 18.sp) + } + Button( + onClick = { + val packet = SocketManager.packPacket( + OPCode.LOGOUT.opcode, JsonObject(mapOf()) + ) + val intent: Intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + + + coroutineScope.launch { + SocketManager.sendPacket(packet, { + + }) + } + ChatManager.clearChatsList() + UserManager.clearUsersList() + + context.startActivity(intent) + finish() + }) { + Text("Выйти из профиля", fontSize = 18.sp) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/preferences/SecurityActivity.kt b/app/src/main/java/com/sffteam/voidclient/preferences/SecurityActivity.kt new file mode 100644 index 0000000..453c80b --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/preferences/SecurityActivity.kt @@ -0,0 +1,409 @@ +package com.sffteam.voidclient.preferences + +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import com.sffteam.voidclient.AccountManager +import com.sffteam.voidclient.OPCode +import com.sffteam.voidclient.SocketManager +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject + + +class SecurityActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val forSettings = mapOf( + "HIDDEN" to mapOf("Контакты" to false, "Никто" to true), + "CONTENT_LEVEL_ACCESS" to mapOf("Весь" to false, "Безопасный" to true), + "CHATS_INVITE" to mapOf("Все" to "ALL", "Контакты" to "CONTACTS"), + "SEARCH_BY_PHONE" to mapOf("Все" to "ALL", "Контакты" to "CONTACTS"), + "INCOMING_CALL" to mapOf("Все" to "ALL", "Контакты" to "CONTACTS"), + ) + + setContent { + val coroutineScope = rememberCoroutineScope() + val settings by AccountManager.settings.collectAsState() + val sheetState = rememberModalBottomSheetState() + + val safeMode = remember { mutableStateOf(settings.safeMode) } + val context = LocalContext.current + var showBottomSheet by remember { mutableStateOf(false) } + + val selectedSettings = remember { mutableStateOf("") } + if (AccountManager.sessionsList.value.isEmpty()) { + val packet = + SocketManager.packPacket(OPCode.SESSIONS.opcode, JsonObject(emptyMap())) + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + AccountManager.processSession(packet.payload["sessions"]!!.jsonArray) + } + }) + } + } + + AppTheme { + LaunchedEffect(settings) { + safeMode.value = settings.safeMode + } + + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, sheetState = sheetState + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(start = 8.dp) + ) { + forSettings[selectedSettings.value]?.forEach { (index, value) -> + Text(index, modifier = Modifier.clickable { + val packet = SocketManager.packPacket(OPCode.SETTINGS_CHANGE.opcode, JsonObject( + mapOf( + "settings" to JsonObject( + mapOf( + "user" to JsonObject( + mapOf( + if (value is String) { + selectedSettings.value to JsonPrimitive(value) + } else if (value is Boolean) { + selectedSettings.value to JsonPrimitive(value) + } else { + selectedSettings.value to JsonPrimitive("") + } + ) + ) + ) + ) + ) + )) + + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + AccountManager.processSettings(packet.payload["user"]!!.jsonObject) + } + }) + } + showBottomSheet = false + } + .padding(8.dp), fontSize = 24.sp) + } + } + } + } + + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceContainer, + ), + title = { + Text("Безопасность") + }, + navigationIcon = { + IconButton({ finish() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + contentDescription = "Меню" + ) + } + }, + ) + }) { + Box( + modifier = Modifier + .padding(it) + .fillMaxSize() + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item() { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + .padding(start = 4.dp, top = 4.dp, end = 4.dp) + ) { + Column(modifier = Modifier + .padding(bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + Icons.Outlined.Lock, + contentDescription = "lol", + modifier = Modifier.size(40.dp).padding(start = 8.dp) + ) + Text("Безопасный режим", fontSize = 24.sp, modifier = Modifier.padding(start = 8.dp)) + + Spacer(modifier = Modifier.weight(1f)) + + Switch( + checked = safeMode.value, + onCheckedChange = { + val packet = SocketManager.packPacket(OPCode.SETTINGS_CHANGE.opcode, JsonObject( + mapOf( + "settings" to JsonObject( + mapOf( + "user" to JsonObject( + mapOf( + "SAFE_MODE" to JsonPrimitive(it) + ) + ) + ) + ) + ) + )) + + coroutineScope.launch { + SocketManager.sendPacket(packet, { packet -> + if (packet.payload is JsonObject) { + AccountManager.processSettings(packet.payload["user"]!!.jsonObject) + } + }) + } + }, + modifier = Modifier.padding(end = 4.dp) + ) + } + + Row(modifier = Modifier + .clickable { + if (settings.safeMode) { + Toast.makeText( + context, + "Отключите безопасный режим, чтобы изменить эту настройку", + Toast.LENGTH_SHORT + ).show() + } else { + selectedSettings.value = "SEARCH_BY_PHONE" + showBottomSheet = true + } + }) { + Text("Найти меня по номеру", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp)) + + val whoCan = if (settings.searchByPhone == "ALL") { + "Все" + } else { + "Контакты" + } + Spacer(modifier = Modifier.weight(1f)) + + if (settings.safeMode) { + Icon( + Icons.Outlined.Lock, + contentDescription = "lol", + modifier = Modifier.size(25.dp).padding(start = 4.dp, end = 4.dp) + ) + } + + Text(whoCan, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp)) + } + + Row(modifier = Modifier + .clickable { + if (settings.safeMode) { + Toast.makeText( + context, + "Отключите безопасный режим, чтобы изменить эту настройку", + Toast.LENGTH_SHORT + ).show() + } else { + selectedSettings.value = "INCOMING_CALL" + showBottomSheet = true + } + }) { + Text("Позвонить", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp)) + + val whoCan = if (settings.incomingCall == "ALL") { + "Все" + } else { + "Контакты" + } + + Spacer(modifier = Modifier.weight(1f)) + + if (settings.safeMode) { + Icon( + Icons.Outlined.Lock, + contentDescription = "lol", + modifier = Modifier.size(25.dp).padding(start = 4.dp, end = 4.dp) + ) + } + Text(whoCan, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp)) + } + + Row(modifier = Modifier + .clickable { + if (settings.safeMode) { + Toast.makeText( + context, + "Отключите безопасный режим, чтобы изменить эту настройку", + Toast.LENGTH_SHORT + ).show() + } else { + selectedSettings.value = "CHATS_INVITE" + showBottomSheet = true + } + }) { + Text("Приглашения в чат", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp)) + + val whoCan = if (settings.chatsInvite == "ALL") { + "Все" + } else { + "Контакты" + } + Spacer(modifier = Modifier.weight(1f)) + + if (settings.safeMode) { + Icon( + Icons.Outlined.Lock, + contentDescription = "lol", + modifier = Modifier.size(25.dp).padding(start = 4.dp, end = 4.dp) + ) + } + + Text(whoCan, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp)) + } + + Row(modifier = Modifier + .clickable { + if (settings.safeMode) { + Toast.makeText( + context, + "Отключите безопасный режим, чтобы изменить эту настройку", + Toast.LENGTH_SHORT + ).show() + } else { + selectedSettings.value = "CONTENT_LEVEL_ACCESS" + showBottomSheet = true + } + }) { + Text("Показывать контент", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp)) + + val content = if (!settings.contentLevelAccess) { + "Весь" + } else { + "Безопасный" + } + Spacer(modifier = Modifier.weight(1f)) + + if (settings.safeMode) { + Icon( + Icons.Outlined.Lock, + contentDescription = "lol", + modifier = Modifier.size(25.dp).padding(start = 4.dp, end = 4.dp) + ) + } + + Text(content, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp)) + } + } + } + } + + item() { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + .padding(start = 4.dp, top = 4.dp, end = 4.dp) + ) { + Column() { + Text( + "Информация", + fontSize = 22.sp, + color = colorScheme.primary, + modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) + ) + + Row(modifier = Modifier + .clickable { + selectedSettings.value = "HIDDEN" + showBottomSheet = true + } + .padding(bottom = 8.dp)) { + Text("Статус \"В сети\"", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp)) + + val hidden = if (settings.hidden) { + "Никто" + } else { + "Контакты" + } + Spacer(modifier = Modifier.weight(1f)) + + Text(hidden, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp)) + } + } + } + } + + item() {} + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/preferences/SettingsActivity.kt b/app/src/main/java/com/sffteam/voidclient/preferences/SettingsActivity.kt new file mode 100644 index 0000000..5f91db0 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/preferences/SettingsActivity.kt @@ -0,0 +1,515 @@ +package com.sffteam.voidclient.preferences + +import android.content.ClipData +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.outlined.ChatBubble +import androidx.compose.material.icons.outlined.Folder +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.Smartphone +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import com.sffteam.voidclient.AccountManager +import com.sffteam.voidclient.ImageViewerActivity +import com.sffteam.voidclient.UserManager +import com.sffteam.voidclient.Utils +import com.sffteam.voidclient.ui.theme.AppTheme +import kotlinx.coroutines.launch +import java.util.Locale.getDefault + +class SettingsActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + AppTheme { + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceContainer, + ), + title = { + Text("Настройки") + }, + navigationIcon = { + IconButton({ finish() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBackIos, + contentDescription = "Меню" + ) + } + }, + ) + }) { + val context = LocalContext.current + val users by UserManager.usersList.collectAsState() + + println("users $users") + val user = users[AccountManager.accountID] + val username = user?.firstName + if (user?.lastName?.isNotEmpty() == true) { + " " + user.lastName + } else { + "" + } + val coroutineScope = rememberCoroutineScope() + val clipboardManager = LocalClipboard.current + + Box( + modifier = Modifier + .padding(it) + .fillMaxSize() + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(top = 12.dp, start = 12.dp, end = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + // Account + item { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (user?.avatarUrl?.isNotEmpty() == true) { + AsyncImage( + model = user.avatarUrl, + contentDescription = "ChatIcon", + modifier = Modifier + .width(100.dp) + .height(100.dp) + .clip(CircleShape) + .align(Alignment.CenterHorizontally) + .clickable { + val intent = Intent(context, ImageViewerActivity::class.java) + + intent.putExtra("isSingleImage", true) + intent.putExtra("image", user.avatarUrl) + + context.startActivity(intent) + }, + contentScale = ContentScale.Crop, + ) + } else { + val fullName = user?.firstName + user?.lastName + val initial = + fullName.split(" ").mapNotNull { it.firstOrNull() } + .take(2).joinToString("").uppercase(getDefault()) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(100.dp) + .height(100.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colors = listOf( + Utils.getColorForAvatar(fullName).first, + Utils.getColorForAvatar(fullName).second + ) + ) + ) + .align(Alignment.CenterHorizontally), + ) { + Text( + text = initial, + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontSize = 25.sp + ) + } + } + + Text( + text = username, + fontSize = 20.sp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .clickable { + coroutineScope.launch { + clipboardManager.setClipEntry( + ClipEntry( + ClipData.newPlainText( + username, username + ) + ) + ) + } + } + ) + + Text( + text = "+${AccountManager.phone}", + fontSize = 16.sp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .clickable { + coroutineScope.launch { + clipboardManager.setClipEntry( + ClipEntry( + ClipData.newPlainText( + "+${AccountManager.phone}", + "+${AccountManager.phone}" + ) + ) + ) + } + } + ) + Text( + text = "ID: ${AccountManager.accountID}", + fontSize = 16.sp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .clickable { + coroutineScope.launch { + clipboardManager.setClipEntry( + ClipEntry( + ClipData.newPlainText( + "${AccountManager.accountID}", + "${AccountManager.accountID}" + ) + ) + ) + } + }) + + Text( + text = "Нажмите на информацию, чтобы скопировать её", + fontSize = 14.sp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .alpha(0.7f) + ) + } + } + + item { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + .clickable { + val intent = + Intent(context, ProfileSettingsActivity::class.java) + + context.startActivity(intent) + }) { + Column() { + Row( + modifier = Modifier.padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + Icons.Filled.AccountCircle, + "lol", + modifier = Modifier + .size(30.dp) + .padding(), + tint = colorScheme.onSecondaryContainer + ) + Text( + "Мой аккаунт", + fontSize = 20.sp, + modifier = Modifier.align(Alignment.CenterVertically), + color = colorScheme.onSecondaryContainer + ) + } + } + } + } + + // Settings + item { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + ) { + Column() { + // For next update +// Row( +// modifier = Modifier +// .padding(12.dp) +// .clickable { +// val intent = +// Intent( +// context, +// ProfileSettingsActivity::class.java +// ) +// +// context.startActivity(intent) +// }, +// horizontalArrangement = Arrangement.spacedBy(6.dp) +// ) { +// Icon( +// Icons.Outlined.Settings, +// "lol", +// modifier = Modifier +// .size(25.dp) +// .padding() +// ) +// Text( +// "Настройки Open MAX", fontSize = 20.sp, +// modifier = Modifier.align(Alignment.CenterVertically) +// ) +// } +// Row( +// modifier = Modifier +// .padding(12.dp) +// .clickable { +// val intent = +// Intent( +// context, +// ChatSettingsActivity::class.java +// ) +// +// context.startActivity(intent) +// }, +// horizontalArrangement = Arrangement.spacedBy(6.dp) +// ) { +// Icon( +// Icons.Outlined.ChatBubble, +// "lol", +// modifier = Modifier +// .size(25.dp) +// .padding() +// ) +// Text( +// "Настройки чатов", fontSize = 20.sp, +// modifier = Modifier.align(Alignment.CenterVertically) +// ) +// } + Row( + modifier = Modifier + .padding(12.dp) + .clickable { + val intent = + Intent( + context, + SecurityActivity::class.java + ) + + context.startActivity(intent) + }, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + Icons.Outlined.Lock, + "lol", + modifier = Modifier + .size(25.dp) + .padding(), + tint = colorScheme.onSecondaryContainer + ) + Text( + "Безопасность", fontSize = 20.sp, + modifier = Modifier.align(Alignment.CenterVertically), + color = colorScheme.onSecondaryContainer + ) + } + + Row( + modifier = Modifier + .padding(12.dp) + .clickable { + val intent = + Intent(context, DevicesActivity::class.java) + + context.startActivity(intent) + }, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + Icons.Outlined.Smartphone, + "lol", + modifier = Modifier + .size(25.dp) + .padding(), + tint = colorScheme.onSecondaryContainer + ) + Text( + "Устройства", + fontSize = 20.sp, + modifier = Modifier.align(Alignment.CenterVertically), + color = colorScheme.onSecondaryContainer + ) + } + + Row( + modifier = Modifier + .padding(12.dp) + .clickable { + Toast.makeText( + context, + "Будет доступно в следующих обновлениях", + Toast.LENGTH_LONG + ).show() + }, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + Icons.Outlined.Notifications, + "lol", + modifier = Modifier + .size(25.dp) + .padding() + .alpha(0.7f), + tint = colorScheme.onSecondaryContainer + ) + Text( + "Уведомления", + fontSize = 20.sp, + modifier = Modifier + .align(Alignment.CenterVertically) + .alpha(0.7f), + color = colorScheme.onSecondaryContainer + ) + } + + Row( + modifier = Modifier + .padding(12.dp) + .clickable { + Toast.makeText( + context, + "Будет доступно в следующих обновлениях", + Toast.LENGTH_LONG + ).show() + }, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + Icons.Outlined.Folder, + "lol", + modifier = Modifier + .size(25.dp) + .padding() + .alpha(0.7f), + tint = colorScheme.onSecondaryContainer + ) + Text( + "Папки с чатами", + fontSize = 20.sp, + modifier = Modifier + .align(Alignment.CenterVertically) + .alpha(0.7f), + color = colorScheme.onSecondaryContainer + ) + } + } + } + } + + item { + Box( + modifier = Modifier + .background( + colorScheme.secondaryContainer, + shape = RoundedCornerShape(20.dp) + ) + .fillMaxWidth() + ) { + Column() { + Row( + modifier = Modifier + .padding(12.dp) + .clickable { + val intent = + Intent(context, AboutActivity::class.java) + + context.startActivity(intent) + }, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + Icons.Outlined.Info, + "lol", + modifier = Modifier + .size(25.dp) + .padding(), + tint = colorScheme.onSecondaryContainer + ) + Text( + "О приложении", + fontSize = 20.sp, + modifier = Modifier.align(Alignment.CenterVertically), + color = colorScheme.onSecondaryContainer + ) + } + } + } + } + } + Text( + "Void Client a1.0.0", + fontSize = 14.sp, + modifier = Modifier + .alpha(0.7f) + .align(Alignment.BottomCenter) + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sffteam/voidclient/ui/theme/Color.kt b/app/src/main/java/com/sffteam/voidclient/ui/theme/Color.kt new file mode 100644 index 0000000..b1974c6 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ui/theme/Color.kt @@ -0,0 +1,225 @@ +package com.sffteam.voidclient.ui.theme +import androidx.compose.ui.graphics.Color + +val primaryLight = Color(0xFF415F91) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFFD6E3FF) +val onPrimaryContainerLight = Color(0xFF284777) +val secondaryLight = Color(0xFF565F71) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFDAE2F9) +val onSecondaryContainerLight = Color(0xFF3E4759) +val tertiaryLight = Color(0xFF705575) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFFAD8FD) +val onTertiaryContainerLight = Color(0xFF573E5C) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF93000A) +val backgroundLight = Color(0xFFF9F9FF) +val onBackgroundLight = Color(0xFF191C20) +val surfaceLight = Color(0xFFF9F9FF) +val onSurfaceLight = Color(0xFF191C20) +val surfaceVariantLight = Color(0xFFE0E2EC) +val onSurfaceVariantLight = Color(0xFF44474E) +val outlineLight = Color(0xFF74777F) +val outlineVariantLight = Color(0xFFC4C6D0) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF2E3036) +val inverseOnSurfaceLight = Color(0xFFF0F0F7) +val inversePrimaryLight = Color(0xFFAAC7FF) +val surfaceDimLight = Color(0xFFD9D9E0) +val surfaceBrightLight = Color(0xFFF9F9FF) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFF3F3FA) +val surfaceContainerLight = Color(0xFFEDEDF4) +val surfaceContainerHighLight = Color(0xFFE7E8EE) +val surfaceContainerHighestLight = Color(0xFFE2E2E9) + +val primaryLightMediumContrast = Color(0xFF133665) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF506DA0) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF2E3647) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF646D80) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF452E4A) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF7F6484) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF740006) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFCF2C27) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFF9F9FF) +val onBackgroundLightMediumContrast = Color(0xFF191C20) +val surfaceLightMediumContrast = Color(0xFFF9F9FF) +val onSurfaceLightMediumContrast = Color(0xFF0F1116) +val surfaceVariantLightMediumContrast = Color(0xFFE0E2EC) +val onSurfaceVariantLightMediumContrast = Color(0xFF33363E) +val outlineLightMediumContrast = Color(0xFF4F525A) +val outlineVariantLightMediumContrast = Color(0xFF6A6D75) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF2E3036) +val inverseOnSurfaceLightMediumContrast = Color(0xFFF0F0F7) +val inversePrimaryLightMediumContrast = Color(0xFFAAC7FF) +val surfaceDimLightMediumContrast = Color(0xFFC5C6CD) +val surfaceBrightLightMediumContrast = Color(0xFFF9F9FF) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA) +val surfaceContainerLightMediumContrast = Color(0xFFE7E8EE) +val surfaceContainerHighLightMediumContrast = Color(0xFFDCDCE3) +val surfaceContainerHighestLightMediumContrast = Color(0xFFD1D1D8) + +val primaryLightHighContrast = Color(0xFF032B5B) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF2A497A) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF232C3D) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF41495B) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF3A2440) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF59405E) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF600004) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF98000A) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFF9F9FF) +val onBackgroundLightHighContrast = Color(0xFF191C20) +val surfaceLightHighContrast = Color(0xFFF9F9FF) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFE0E2EC) +val onSurfaceVariantLightHighContrast = Color(0xFF000000) +val outlineLightHighContrast = Color(0xFF292C33) +val outlineVariantLightHighContrast = Color(0xFF464951) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF2E3036) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFAAC7FF) +val surfaceDimLightHighContrast = Color(0xFFB8B8BF) +val surfaceBrightLightHighContrast = Color(0xFFF9F9FF) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFF0F0F7) +val surfaceContainerLightHighContrast = Color(0xFFE2E2E9) +val surfaceContainerHighLightHighContrast = Color(0xFFD3D4DB) +val surfaceContainerHighestLightHighContrast = Color(0xFFC5C6CD) + +val primaryDark = Color(0xFFAAC7FF) +val onPrimaryDark = Color(0xFF0A305F) +val primaryContainerDark = Color(0xFF284777) +val onPrimaryContainerDark = Color(0xFFD6E3FF) +val secondaryDark = Color(0xFFBEC6DC) +val onSecondaryDark = Color(0xFF283141) +val secondaryContainerDark = Color(0xFF3E4759) +val onSecondaryContainerDark = Color(0xFFDAE2F9) +val tertiaryDark = Color(0xFFDDBCE0) +val onTertiaryDark = Color(0xFF3F2844) +val tertiaryContainerDark = Color(0xFF573E5C) +val onTertiaryContainerDark = Color(0xFFFAD8FD) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF111318) +val onBackgroundDark = Color(0xFFE2E2E9) +val surfaceDark = Color(0xFF111318) +val onSurfaceDark = Color(0xFFE2E2E9) +val surfaceVariantDark = Color(0xFF44474E) +val onSurfaceVariantDark = Color(0xFFC4C6D0) +val outlineDark = Color(0xFF8E9099) +val outlineVariantDark = Color(0xFF44474E) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFE2E2E9) +val inverseOnSurfaceDark = Color(0xFF2E3036) +val inversePrimaryDark = Color(0xFF415F91) +val surfaceDimDark = Color(0xFF111318) +val surfaceBrightDark = Color(0xFF37393E) +val surfaceContainerLowestDark = Color(0xFF0C0E13) +val surfaceContainerLowDark = Color(0xFF191C20) +val surfaceContainerDark = Color(0xFF1D2024) +val surfaceContainerHighDark = Color(0xFF282A2F) +val surfaceContainerHighestDark = Color(0xFF33353A) + +val primaryDarkMediumContrast = Color(0xFFCDDDFF) +val onPrimaryDarkMediumContrast = Color(0xFF002551) +val primaryContainerDarkMediumContrast = Color(0xFF7491C7) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFD4DCF2) +val onSecondaryDarkMediumContrast = Color(0xFF1D2636) +val secondaryContainerDarkMediumContrast = Color(0xFF8891A5) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFFF3D2F7) +val onTertiaryDarkMediumContrast = Color(0xFF331D39) +val tertiaryContainerDarkMediumContrast = Color(0xFFA487A9) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFD2CC) +val onErrorDarkMediumContrast = Color(0xFF540003) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF111318) +val onBackgroundDarkMediumContrast = Color(0xFFE2E2E9) +val surfaceDarkMediumContrast = Color(0xFF111318) +val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkMediumContrast = Color(0xFF44474E) +val onSurfaceVariantDarkMediumContrast = Color(0xFFDADCE6) +val outlineDarkMediumContrast = Color(0xFFAFB2BB) +val outlineVariantDarkMediumContrast = Color(0xFF8E9099) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F) +val inversePrimaryDarkMediumContrast = Color(0xFF294878) +val surfaceDimDarkMediumContrast = Color(0xFF111318) +val surfaceBrightDarkMediumContrast = Color(0xFF43444A) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF06070C) +val surfaceContainerLowDarkMediumContrast = Color(0xFF1B1E22) +val surfaceContainerDarkMediumContrast = Color(0xFF26282D) +val surfaceContainerHighDarkMediumContrast = Color(0xFF313238) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C3E43) + +val primaryDarkHighContrast = Color(0xFFEBF0FF) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFA6C3FC) +val onPrimaryContainerDarkHighContrast = Color(0xFF000B20) +val secondaryDarkHighContrast = Color(0xFFEBF0FF) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFBAC3D8) +val onSecondaryContainerDarkHighContrast = Color(0xFF030B1A) +val tertiaryDarkHighContrast = Color(0xFFFFE9FF) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFFD8B8DC) +val onTertiaryContainerDarkHighContrast = Color(0xFF16041D) +val errorDarkHighContrast = Color(0xFFFFECE9) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFAEA4) +val onErrorContainerDarkHighContrast = Color(0xFF220001) +val backgroundDarkHighContrast = Color(0xFF111318) +val onBackgroundDarkHighContrast = Color(0xFFE2E2E9) +val surfaceDarkHighContrast = Color(0xFF111318) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF44474E) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) +val outlineDarkHighContrast = Color(0xFFEEEFF9) +val outlineVariantDarkHighContrast = Color(0xFFC0C2CC) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF294878) +val surfaceDimDarkHighContrast = Color(0xFF111318) +val surfaceBrightDarkHighContrast = Color(0xFF4E5056) +val surfaceContainerLowestDarkHighContrast = Color(0xFF000000) +val surfaceContainerLowDarkHighContrast = Color(0xFF1D2024) +val surfaceContainerDarkHighContrast = Color(0xFF2E3036) +val surfaceContainerHighDarkHighContrast = Color(0xFF393B41) +val surfaceContainerHighestDarkHighContrast = Color(0xFF45474C) + + + + + + + diff --git a/app/src/main/java/com/sffteam/voidclient/ui/theme/Theme.kt b/app/src/main/java/com/sffteam/voidclient/ui/theme/Theme.kt new file mode 100644 index 0000000..9a5a057 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ui/theme/Theme.kt @@ -0,0 +1,280 @@ +package com.sffteam.voidclient.ui.theme +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext + +private val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +private val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) + +private val mediumContrastLightColorScheme = lightColorScheme( + primary = primaryLightMediumContrast, + onPrimary = onPrimaryLightMediumContrast, + primaryContainer = primaryContainerLightMediumContrast, + onPrimaryContainer = onPrimaryContainerLightMediumContrast, + secondary = secondaryLightMediumContrast, + onSecondary = onSecondaryLightMediumContrast, + secondaryContainer = secondaryContainerLightMediumContrast, + onSecondaryContainer = onSecondaryContainerLightMediumContrast, + tertiary = tertiaryLightMediumContrast, + onTertiary = onTertiaryLightMediumContrast, + tertiaryContainer = tertiaryContainerLightMediumContrast, + onTertiaryContainer = onTertiaryContainerLightMediumContrast, + error = errorLightMediumContrast, + onError = onErrorLightMediumContrast, + errorContainer = errorContainerLightMediumContrast, + onErrorContainer = onErrorContainerLightMediumContrast, + background = backgroundLightMediumContrast, + onBackground = onBackgroundLightMediumContrast, + surface = surfaceLightMediumContrast, + onSurface = onSurfaceLightMediumContrast, + surfaceVariant = surfaceVariantLightMediumContrast, + onSurfaceVariant = onSurfaceVariantLightMediumContrast, + outline = outlineLightMediumContrast, + outlineVariant = outlineVariantLightMediumContrast, + scrim = scrimLightMediumContrast, + inverseSurface = inverseSurfaceLightMediumContrast, + inverseOnSurface = inverseOnSurfaceLightMediumContrast, + inversePrimary = inversePrimaryLightMediumContrast, + surfaceDim = surfaceDimLightMediumContrast, + surfaceBright = surfaceBrightLightMediumContrast, + surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, + surfaceContainerLow = surfaceContainerLowLightMediumContrast, + surfaceContainer = surfaceContainerLightMediumContrast, + surfaceContainerHigh = surfaceContainerHighLightMediumContrast, + surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, +) + +private val highContrastLightColorScheme = lightColorScheme( + primary = primaryLightHighContrast, + onPrimary = onPrimaryLightHighContrast, + primaryContainer = primaryContainerLightHighContrast, + onPrimaryContainer = onPrimaryContainerLightHighContrast, + secondary = secondaryLightHighContrast, + onSecondary = onSecondaryLightHighContrast, + secondaryContainer = secondaryContainerLightHighContrast, + onSecondaryContainer = onSecondaryContainerLightHighContrast, + tertiary = tertiaryLightHighContrast, + onTertiary = onTertiaryLightHighContrast, + tertiaryContainer = tertiaryContainerLightHighContrast, + onTertiaryContainer = onTertiaryContainerLightHighContrast, + error = errorLightHighContrast, + onError = onErrorLightHighContrast, + errorContainer = errorContainerLightHighContrast, + onErrorContainer = onErrorContainerLightHighContrast, + background = backgroundLightHighContrast, + onBackground = onBackgroundLightHighContrast, + surface = surfaceLightHighContrast, + onSurface = onSurfaceLightHighContrast, + surfaceVariant = surfaceVariantLightHighContrast, + onSurfaceVariant = onSurfaceVariantLightHighContrast, + outline = outlineLightHighContrast, + outlineVariant = outlineVariantLightHighContrast, + scrim = scrimLightHighContrast, + inverseSurface = inverseSurfaceLightHighContrast, + inverseOnSurface = inverseOnSurfaceLightHighContrast, + inversePrimary = inversePrimaryLightHighContrast, + surfaceDim = surfaceDimLightHighContrast, + surfaceBright = surfaceBrightLightHighContrast, + surfaceContainerLowest = surfaceContainerLowestLightHighContrast, + surfaceContainerLow = surfaceContainerLowLightHighContrast, + surfaceContainer = surfaceContainerLightHighContrast, + surfaceContainerHigh = surfaceContainerHighLightHighContrast, + surfaceContainerHighest = surfaceContainerHighestLightHighContrast, +) + +private val mediumContrastDarkColorScheme = darkColorScheme( + primary = primaryDarkMediumContrast, + onPrimary = onPrimaryDarkMediumContrast, + primaryContainer = primaryContainerDarkMediumContrast, + onPrimaryContainer = onPrimaryContainerDarkMediumContrast, + secondary = secondaryDarkMediumContrast, + onSecondary = onSecondaryDarkMediumContrast, + secondaryContainer = secondaryContainerDarkMediumContrast, + onSecondaryContainer = onSecondaryContainerDarkMediumContrast, + tertiary = tertiaryDarkMediumContrast, + onTertiary = onTertiaryDarkMediumContrast, + tertiaryContainer = tertiaryContainerDarkMediumContrast, + onTertiaryContainer = onTertiaryContainerDarkMediumContrast, + error = errorDarkMediumContrast, + onError = onErrorDarkMediumContrast, + errorContainer = errorContainerDarkMediumContrast, + onErrorContainer = onErrorContainerDarkMediumContrast, + background = backgroundDarkMediumContrast, + onBackground = onBackgroundDarkMediumContrast, + surface = surfaceDarkMediumContrast, + onSurface = onSurfaceDarkMediumContrast, + surfaceVariant = surfaceVariantDarkMediumContrast, + onSurfaceVariant = onSurfaceVariantDarkMediumContrast, + outline = outlineDarkMediumContrast, + outlineVariant = outlineVariantDarkMediumContrast, + scrim = scrimDarkMediumContrast, + inverseSurface = inverseSurfaceDarkMediumContrast, + inverseOnSurface = inverseOnSurfaceDarkMediumContrast, + inversePrimary = inversePrimaryDarkMediumContrast, + surfaceDim = surfaceDimDarkMediumContrast, + surfaceBright = surfaceBrightDarkMediumContrast, + surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, + surfaceContainerLow = surfaceContainerLowDarkMediumContrast, + surfaceContainer = surfaceContainerDarkMediumContrast, + surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, + surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, +) + +private val highContrastDarkColorScheme = darkColorScheme( + primary = primaryDarkHighContrast, + onPrimary = onPrimaryDarkHighContrast, + primaryContainer = primaryContainerDarkHighContrast, + onPrimaryContainer = onPrimaryContainerDarkHighContrast, + secondary = secondaryDarkHighContrast, + onSecondary = onSecondaryDarkHighContrast, + secondaryContainer = secondaryContainerDarkHighContrast, + onSecondaryContainer = onSecondaryContainerDarkHighContrast, + tertiary = tertiaryDarkHighContrast, + onTertiary = onTertiaryDarkHighContrast, + tertiaryContainer = tertiaryContainerDarkHighContrast, + onTertiaryContainer = onTertiaryContainerDarkHighContrast, + error = errorDarkHighContrast, + onError = onErrorDarkHighContrast, + errorContainer = errorContainerDarkHighContrast, + onErrorContainer = onErrorContainerDarkHighContrast, + background = backgroundDarkHighContrast, + onBackground = onBackgroundDarkHighContrast, + surface = surfaceDarkHighContrast, + onSurface = onSurfaceDarkHighContrast, + surfaceVariant = surfaceVariantDarkHighContrast, + onSurfaceVariant = onSurfaceVariantDarkHighContrast, + outline = outlineDarkHighContrast, + outlineVariant = outlineVariantDarkHighContrast, + scrim = scrimDarkHighContrast, + inverseSurface = inverseSurfaceDarkHighContrast, + inverseOnSurface = inverseOnSurfaceDarkHighContrast, + inversePrimary = inversePrimaryDarkHighContrast, + surfaceDim = surfaceDimDarkHighContrast, + surfaceBright = surfaceBrightDarkHighContrast, + surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, + surfaceContainerLow = surfaceContainerLowDarkHighContrast, + surfaceContainer = surfaceContainerDarkHighContrast, + surfaceContainerHigh = surfaceContainerHighDarkHighContrast, + surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, +) + +@Immutable +data class ColorFamily( + val color: Color, + val onColor: Color, + val colorContainer: Color, + val onColorContainer: Color +) + +val unspecified_scheme = ColorFamily( + Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified +) + +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable() () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> darkScheme + else -> lightScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = AppTypography, + content = content + ) +} + diff --git a/app/src/main/java/com/sffteam/voidclient/ui/theme/Type.kt b/app/src/main/java/com/sffteam/voidclient/ui/theme/Type.kt new file mode 100644 index 0000000..faa82e1 --- /dev/null +++ b/app/src/main/java/com/sffteam/voidclient/ui/theme/Type.kt @@ -0,0 +1,9 @@ +package com.sffteam.voidclient.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val AppTypography = Typography() diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..a15ab29 --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat_edit.xml b/app/src/main/res/layout/activity_chat_edit.xml new file mode 100644 index 0000000..6e02d67 --- /dev/null +++ b/app/src/main/res/layout/activity_chat_edit.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat_view.xml b/app/src/main/res/layout/activity_chat_view.xml new file mode 100644 index 0000000..9748381 --- /dev/null +++ b/app/src/main/res/layout/activity_chat_view.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_image_viewer.xml b/app/src/main/res/layout/activity_image_viewer.xml new file mode 100644 index 0000000..0fa3917 --- /dev/null +++ b/app/src/main/res/layout/activity_image_viewer.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_password_check2.xml b/app/src/main/res/layout/activity_password_check2.xml new file mode 100644 index 0000000..67240c0 --- /dev/null +++ b/app/src/main/res/layout/activity_password_check2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profile_view.xml b/app/src/main/res/layout/activity_profile_view.xml new file mode 100644 index 0000000..6355027 --- /dev/null +++ b/app/src/main/res/layout/activity_profile_view.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c4a603d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..c4a603d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..88a6016 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..cb1fb06 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1efd00e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..30c3270 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..d018e78 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..51d9444 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..a9f2815 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..6c75590 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..a966222 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..fb68bd2 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..abb5c86 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..6f4ec6c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..a9c00bb Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..9ad8127 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..32aceda Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 0000000..73862c4 --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..3dc05ce --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v23/themes.xml b/app/src/main/res/values-v23/themes.xml new file mode 100644 index 0000000..7b3958f --- /dev/null +++ b/app/src/main/res/values-v23/themes.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 0000000..73862c4 --- /dev/null +++ b/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 0000000..73862c4 --- /dev/null +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..a082d2e --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..73862c4 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..92c6580 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + CodeActivity + ChatListActivity + ChatActivity + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..9e3a482 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..4df9255 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/sffteam/voidclient/ExampleUnitTest.kt b/app/src/test/java/com/sffteam/voidclient/ExampleUnitTest.kt new file mode 100644 index 0000000..0e78dae --- /dev/null +++ b/app/src/test/java/com/sffteam/voidclient/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.sffteam.openmax + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..952b930 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..20e2a01 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..b90e813 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,82 @@ +[versions] +agp = "8.13.0" +appcompat = "1.7.1" +coilCompose = "3.3.0" +datastorePreferencesCoreVersion = "1.1.7" +guava = "33.5.0-android" +kotlin = "2.2.21" +coreKtx = "1.17.0" +kotlinxDatetime = "0.7.1" +lifecycleRuntimeKtx = "2.9.4" +activityCompose = "1.11.0" +composeBom = "2025.10.01" +lifecycleViewmodelCompose = "2.10.0" +activity = "1.11.0" +kotlinxSerializationJson = "1.9.0" +material = "1.13.0" +compiler = "3.2.0-alpha11" +datastoreCoreVersion = "1.1.7" +roomKtx = "2.8.4" +foundation = "1.9.5" +textflowMaterial3 = "1.2.1" +material3 = "1.4.0" +ui = "1.9.5" +adaptive = "1.2.0" +material3WindowSizeClass = "1.4.0" +uiText = "1.10.0" +animation = "1.10.0" +animationCore = "1.10.0" +foundationLayout = "1.10.0" +navigationFragmentKtx = "2.6.0" +navigationUiKtx = "2.6.0" +foundationVersion = "1.10.0" +constraintlayout = "2.2.1" +media3Exoplayer = "1.9.2" + +[libraries] +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferencesCoreVersion" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-compiler = { group = "androidx.databinding", name = "compiler", version.ref = "compiler" } +androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCoreVersion" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } +textflow-material3 = { module = "io.github.oleksandrbalan:textflow-material3", version.ref = "textflowMaterial3" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } +androidx-compose-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" } +androidx-compose-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" } +autolinktext = { module = "sh.calvin.autolinktext:autolinktext", version = "2.0.2" } +androidx-compose-ui-text = { group = "androidx.compose.ui", name = "ui-text", version.ref = "uiText" } +androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "animation" } +androidx-compose-animation-core = { group = "androidx.compose.animation", name = "animation-core", version.ref = "animationCore" } +androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" } +androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } +androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } +androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3Exoplayer" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..88ba730 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +#Sun Nov 02 14:28:35 EET 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..e8b481e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,24 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Void Client" +include(":app") + \ No newline at end of file