From 290a88fd90be2f5df499435d45367f25462f2327 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Feb 2022 20:30:21 +0000 Subject: [PATCH] list of chats and chat messages (#327) --- .../java/chat/simplex/app/MainActivity.kt | 35 ++++++--- .../main/java/chat/simplex/app/MainPage.kt | 10 ++- .../main/java/chat/simplex/app/SimplexApp.kt | 16 +--- .../java/chat/simplex/app/model/ChatModel.kt | 18 +++-- .../java/chat/simplex/app/model/SimpleXAPI.kt | 37 ++++++++-- .../chat/simplex/app/views/TerminalView.kt | 5 +- .../chat/simplex/app/views/WelcomeView.kt | 11 +-- .../chat/simplex/app/views/chat/ChatView.kt | 62 +++++++++++++++- .../app/views/chat/item/ChatItemView.kt | 2 +- .../app/views/chatlist/ChatListView.kt | 74 ++++++++++++++++++- .../app/views/chatlist/ChatPreviewView.kt | 28 ++++++- 11 files changed, 245 insertions(+), 53 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 5870e1fa1d..4a13ded802 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -13,9 +13,10 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import chat.simplex.app.views.DetailView -import chat.simplex.app.views.TerminalView -import chat.simplex.app.views.WelcomeView +import chat.simplex.app.model.ChatModel +import chat.simplex.app.views.* +import chat.simplex.app.views.chat.ChatView +import chat.simplex.app.views.chatlist.* class MainActivity: ComponentActivity() { @@ -25,7 +26,7 @@ class MainActivity: ComponentActivity() { super.onCreate(savedInstanceState) setContent { SimpleXTheme { - Navigation(viewModel=viewModel) + Navigation(viewModel.chatModel) } } } @@ -36,18 +37,28 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application) } @Composable -fun Navigation(viewModel: SimplexViewModel) { - val navController = rememberNavController() +fun Navigation(chatModel: ChatModel) { + val nav = rememberNavController() - NavHost(navController=navController, startDestination=Pages.Home.route){ + NavHost(navController = nav, startDestination=Pages.Home.route){ composable(route=Pages.Home.route){ - MainPage(vm = viewModel, navController = navController) + MainPage(chatModel, nav) } composable(route = Pages.Welcome.route){ - WelcomeView(vm = viewModel) {navController.navigate(Pages.Home.route) { popUpTo(Pages.Home.route) { inclusive = true }}} + WelcomeView(chatModel) { + nav.navigate(Pages.Home.route) { + popUpTo(Pages.Home.route) { inclusive = true } + } + } + } + composable(route = Pages.Chats.route) { + ChatListView(chatModel, nav) + } + composable(route = Pages.Chat.route) { + ChatView(chatModel, nav) } composable(route = Pages.Terminal.route) { - TerminalView(chatModel = viewModel.chatModel, navController = navController) + TerminalView(chatModel, navController = nav) } composable( Pages.TerminalItemDetails.route + "/{identifier}", @@ -56,7 +67,7 @@ fun Navigation(viewModel: SimplexViewModel) { type = NavType.LongType } ) - ) { entry -> DetailView( entry.arguments!!.getLong("identifier"), viewModel.chatModel.terminalItems, navController) } + ) { entry -> DetailView( entry.arguments!!.getLong("identifier"), chatModel.terminalItems, nav) } } } @@ -65,4 +76,6 @@ sealed class Pages(val route: String) { object Terminal : Pages("terminal") object Welcome : Pages("welcome") object TerminalItemDetails : Pages("details") + object Chats: Pages("chats") + object Chat: Pages("chat") } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt index d7abb01223..f59b66d05f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt @@ -2,13 +2,15 @@ package chat.simplex.app import androidx.compose.runtime.Composable import androidx.navigation.NavController +import chat.simplex.app.model.ChatModel import chat.simplex.app.views.TerminalView import chat.simplex.app.views.WelcomeView +import chat.simplex.app.views.chatlist.ChatListView @Composable -fun MainPage(vm: SimplexViewModel, navController: NavController) { - if (vm.chatModel.currentUser.value == null) WelcomeView(vm=vm) { - navController.navigate(Pages.Terminal.route) +fun MainPage(chatModel: ChatModel, nav: NavController) { + if (chatModel.currentUser.value == null) WelcomeView(chatModel) { + nav.navigate(Pages.Chats.route) } - else TerminalView(vm.chatModel, navController) + else ChatListView(chatModel, nav) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 8adeceecf6..b6efb56ffd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -3,6 +3,8 @@ package chat.simplex.app import android.app.Application import android.net.LocalServerSocket import android.util.Log +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList import chat.simplex.app.model.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -35,19 +37,7 @@ class SimplexApp: Application() { GlobalScope.launch { withContext(Dispatchers.Main) { var user = controller.apiGetActiveUser() - controller.setCurrentUser(user) - if (user == null) { - user = controller.apiCreateActiveUser(Profile("android", "Android test")) - } - Log.d("SIMPLEX (user)", user.toString()) - chatModel.currentUser = user - try { - controller.apiStartChat() - Log.d("SIMPLEX", "started chat") - } catch(e: Error) { - Log.d("SIMPLEX", "failed starting chat $e") -// throw e - } + if (user != null) controller.startChat(user) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index f8a7082595..2fd3de45f6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -2,14 +2,15 @@ package chat.simplex.app.model import androidx.compose.runtime.* import kotlinx.serialization.* +import java.util.* class ChatModel(val controller: ChatController) { var currentUser = mutableStateOf(null) - var terminalItems = mutableStateListOf() + var chats = mutableStateListOf() + var chatId = mutableStateOf(null) + var chatItems = mutableStateListOf() - fun setCurrentUser(u: User?){ - currentUser = mutableStateOf(u) - } + var terminalItems = mutableStateListOf() companion object { val sampleData: ChatModel get() { @@ -36,7 +37,7 @@ class User ( val localDisplayName: String, val profile: Profile, val activeUser: Boolean -) : NamedChat { +): NamedChat { override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName @@ -61,6 +62,7 @@ interface NamedChat { } interface SomeChat { + val chatType: ChatType val localDisplayName: String val id: ChatId val apiId: Long @@ -102,6 +104,7 @@ class Chat ( sealed class ChatInfo: SomeChat, NamedChat { @Serializable @SerialName("direct") class Direct(val contact: Contact): ChatInfo() { + override val chatType get() = ChatType.Direct override val localDisplayName get() = contact.localDisplayName override val id get() = contact.id override val apiId get() = contact.apiId @@ -116,6 +119,7 @@ sealed class ChatInfo: SomeChat, NamedChat { @Serializable @SerialName("group") class Group(val groupInfo: GroupInfo): ChatInfo() { + override val chatType get() = ChatType.Group override val localDisplayName get() = groupInfo.localDisplayName override val id get() = groupInfo.id override val apiId get() = groupInfo.apiId @@ -130,6 +134,7 @@ sealed class ChatInfo: SomeChat, NamedChat { @Serializable @SerialName("contactRequest") class ContactRequest(val contactRequest: UserContactRequest): ChatInfo() { + override val chatType get() = ChatType.ContactRequest override val localDisplayName get() = contactRequest.localDisplayName override val id get() = contactRequest.id override val apiId get() = contactRequest.apiId @@ -153,6 +158,7 @@ class Contact( // no serializer for type Date? // val createdAt: Date ): SomeChat, NamedChat { + override val chatType get() = ChatType.Direct override val id get() = "@$contactId" override val apiId get() = contactId override val ready get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" @@ -197,6 +203,7 @@ class GroupInfo ( val groupProfile: GroupProfile, // var createdAt: Date ): SomeChat, NamedChat { + override val chatType get() = ChatType.Group override val id get() = "#$groupId" override val apiId get() = groupId override val ready get() = true @@ -257,6 +264,7 @@ class UserContactRequest ( val profile: Profile // val createdAt: Date ): SomeChat, NamedChat { + override val chatType get() = ChatType.ContactRequest override val id get() = "<@$contactRequestId" override val apiId get() = contactRequestId override val ready get() = true diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index bca01a174d..72b4f048ad 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1,6 +1,9 @@ package chat.simplex.app.model import android.util.Log +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.toMutableStateList import chat.simplex.app.chatRecvMsg import chat.simplex.app.chatSendCmd import kotlinx.coroutines.Dispatchers @@ -21,8 +24,18 @@ open class ChatController(val ctrl: ChatCtrl) { fun setModel(m: ChatModel) { chatModel = m } - suspend fun setCurrentUser(u: User?){ - chatModel!!.setCurrentUser(u) + + suspend fun startChat(u: User) { + chatModel!!.currentUser = mutableStateOf(u) + Log.d("SIMPLEX (user)", u.toString()) + apiStartChat() + try { + Log.d("SIMPLEX", "started chat") + chatModel!!.chats = apiGetChats().toMutableStateList() + } catch(e: Error) { + Log.d("SIMPLEX", "failed starting chat $e") + throw e + } } fun startReceiver() { @@ -75,12 +88,26 @@ open class ChatController(val ctrl: ChatCtrl) { throw Error("failed starting chat: ${r.toString()}") } - suspend fun apiGetChats() { + suspend fun apiGetChats(): List { val r = sendCmd(CC.ApiGetChats()) - if (r is CR.ApiChats ) return + if (r is CR.ApiChats ) return r.chats throw Error("failed getting the list of chats: ${r.toString()}") } + suspend fun apiGetChat(type: ChatType, id: Long): Chat? { + val r = sendCmd(CC.ApiGetChat(type, id)) + if (r is CR.ApiChat ) return r.chat + Log.d("SIMPLEX", "apiGetChat bad response: ${r.toString()}") + return null + } + + suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? { + val r = sendCmd(CC.ApiSendMessage(type, id, mc)) + if (r is CR.NewChatItem ) return r.chatItem + Log.d("SIMPLEX", "apiSendMessage bad response: ${r.toString()}") + return null + } + class Mock: ChatController(0) {} } @@ -125,7 +152,7 @@ abstract class CC { } companion object { - fun chatRef(type: ChatType, id: Long) = "${type}${id}" + fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index bc7705f5e6..891903155d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -31,6 +31,9 @@ import kotlinx.coroutines.withContext @Composable fun TerminalView(chatModel: ChatModel, navController: NavController) { Column { + Button(onClick = { navController.popBackStack() }) { + Text("Back") + } TerminalLog(chatModel.terminalItems, navController) SendMsgView(sendMessage = { cmd -> GlobalScope.launch { @@ -59,7 +62,7 @@ fun TerminalLog(terminalItems: List, navController: NavController) @Composable fun DetailView(identifier: Long, terminalItems: List, navController: NavController){ Column( - modifier=Modifier.verticalScroll(rememberScrollState()) + modifier = Modifier.verticalScroll(rememberScrollState()) ) { Button(onClick = { navController.popBackStack() }) { Text("Back") diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index 30a4334a0a..d2d92f27e8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -14,6 +14,7 @@ import androidx.compose.material.Button import androidx.compose.ui.unit.dp import androidx.compose.ui.Modifier import chat.simplex.app.SimplexViewModel +import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Profile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -21,7 +22,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable -fun WelcomeView(vm: SimplexViewModel, routeHome: () -> Unit) { +fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { @@ -33,12 +34,12 @@ fun WelcomeView(vm: SimplexViewModel, routeHome: () -> Unit) { Spacer(Modifier.height(8.dp)) Text("We don't store any of your contacts or messages (once delivered) on the servers.") Spacer(Modifier.height(24.dp)) - CreateProfilePanel(vm, routeHome) + CreateProfilePanel(chatModel, routeHome) } } @Composable -fun CreateProfilePanel(vm: SimplexViewModel, routeHome: () -> Unit) { +fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) { var displayName by remember { mutableStateOf("") } var fullName by remember { mutableStateOf("") } @@ -52,10 +53,10 @@ fun CreateProfilePanel(vm: SimplexViewModel, routeHome: () -> Unit) { Button(onClick={ GlobalScope.launch { withContext(Dispatchers.Main) { - val user = vm.chatModel.controller.apiCreateActiveUser( + val user = chatModel.controller.apiCreateActiveUser( Profile(displayName, fullName) ) - vm.chatModel.setCurrentUser(user) + chatModel.controller.startChat(user) routeHome() } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 4efa2c50d0..312e0b84bc 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -1,10 +1,66 @@ package chat.simplex.app.views.chat +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable -import chat.simplex.app.model.Chat +import androidx.compose.runtime.MutableState +import androidx.navigation.NavController +import chat.simplex.app.model.* +import chat.simplex.app.views.chat.item.ChatItemView +import chat.simplex.app.views.chatlist.ChatPreviewView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable -fun ChatView(chat: Chat) { - Text("ChatView") +fun ChatView(chatModel: ChatModel, nav: NavController) { + if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { + val chat: Chat = chatModel.chats.first { chat -> chat.chatInfo.id == chatModel.chatId.value } + Column { + ChatInfoToolbar(chat, nav) + ChatItemsList(chatModel.chatItems) + SendMsgView(sendMessage = { msg -> + GlobalScope.launch { + withContext(Dispatchers.Main) { + // show "in progress" + val cInfo = chat.chatInfo + val newItem = chatModel.controller.apiSendMessage( + type = cInfo.chatType, + id = cInfo.apiId, + mc = MsgContent.MCText(msg) + ) + // hide "in progress" + // TODO add new item + } + } + }) + } + } +} + +@Composable +fun ChatInfoToolbar(chat: Chat, nav: NavController) { + Row { + Button(onClick = { nav.popBackStack() }) { + Text("Back") + } + Column { + Text(chat.chatInfo.displayName) + Text(chat.chatInfo.fullName) + } + } +} + +@Composable +fun ChatItemsList(chatItems: List) { + LazyColumn { + items(chatItems) { cItem -> + ChatItemView(cItem) + } + } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 10fc3c9f9c..1aba1845fc 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -6,5 +6,5 @@ import chat.simplex.app.model.ChatItem @Composable fun ChatItemView(chatItem: ChatItem) { - Text("ChatItemView") + Text(chatItem.content.text) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 634af6248d..fa1774ec99 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -1,10 +1,78 @@ package chat.simplex.app.views.chatlist +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable -import chat.simplex.app.model.ChatModel +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import chat.simplex.app.Pages +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.TerminalView +import chat.simplex.app.views.chat.SendMsgView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable -fun ChatListView(chatModel: ChatModel) { - Text("ChatListView") +fun ChatListView(chatModel: ChatModel, navController: NavController) { + Column(modifier = Modifier.padding(all = 8.dp)) { + ChatListToolbar() + Button (onClick = { navController.navigate(Pages.Terminal.route) }) { + Text("Terminal") + } + ChatList(chatModel, navController) + } } + +@Composable +fun ChatListToolbar() { + Text("ChatListToolbar") +} + +@Composable +fun ChatList(chatModel: ChatModel, navController: NavController) { + LazyColumn { + items(chatModel.chats) { chat -> + Button(onClick = { + GlobalScope.launch { + withContext(Dispatchers.Main) { + val cInfo = chat.chatInfo + val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) + if (chat != null ) { + chatModel.chatId = mutableStateOf(cInfo.id) + chatModel.chatItems = chat.chatItems.toMutableStateList() + navController.navigate(Pages.Chat.route) + } else { + // TODO show error? or will apiGetChat show it + } + } + } + }) { + ChatPreviewView(chat) + } + } + } +} + +//@Preview +//@Composable +//fun PreviewSendMsgView() { +// SimpleXTheme { +// ChatListView( +// chats = listOf( +// Chat() +// ), +// +// ) +// } +//} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 993e9b96a5..7954ed4c6a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -1,10 +1,34 @@ package chat.simplex.app.views.chatlist +import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.Composable -import chat.simplex.app.model.Chat +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme @Composable fun ChatPreviewView(chat: Chat) { - Text("ChatPreviewView") + Column(modifier = Modifier.padding(all = 8.dp)) { + Text(chat.chatInfo.chatViewName) + if (chat.chatItems.count() > 0) { + Text(chat.chatItems.last().content.text) + } + } +} + +@Preview +@Composable +fun ChatPreviewView() { + SimpleXTheme { + ChatPreviewView( + chat = Chat( + chatInfo = ChatInfo.Direct.sampleData, + chatItems = listOf(), + chatStats = Chat.ChatStats() + ) + ) + } }