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