chats in android app (#324)

* view placeholders for chats list and chat views

* classes for chats

* set the user to the model

* use Long for IDs

* chats/messages API (not working yet)

* android api works

* line breaks
This commit is contained in:
Evgeny Poberezkin
2022-02-17 17:15:49 +00:00
committed by GitHub
parent 9e46b5117d
commit 423f54e95d
11 changed files with 437 additions and 57 deletions

View File

@@ -7,6 +7,8 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/deploymentTargetDropDown.xml
/.idea/misc.xml
.DS_Store
/build
/captures

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="../../../../../../../layout/compose-model-1644940819446.xml" value="0.4484797297297297" />
<entry key="../../../../../../../layout/compose-model-1644941851914.xml" value="0.28378378378378377" />
<entry key="../../../../../../../layout/compose-model-1644956742665.xml" value="1.0" />
<entry key="../../../../../../../layout/compose-model-1644963789622.xml" value="0.8420454545454545" />
<entry key="../../../../../../../layout/compose-model-1645006324692.xml" value="0.36363636363636365" />
<entry key="../../../../../../../layout/compose-model-1645006342272.xml" value="0.36363636363636365" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@@ -37,9 +37,10 @@ class SimplexApp: Application() {
var user = controller.apiGetActiveUser()
controller.setCurrentUser(user)
if (user == null) {
// user = controller.apiCreateActiveUser(Profile("android", "Android test"))
user = controller.apiCreateActiveUser(Profile("android", "Android test"))
}
Log.d("SIMPLEX (user)", user.toString())
chatModel.currentUser = user
try {
controller.apiStartChat()
Log.d("SIMPLEX", "started chat")

View File

@@ -1,8 +1,7 @@
package chat.simplex.app.model
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import kotlinx.serialization.Serializable
import androidx.compose.runtime.*
import kotlinx.serialization.*
class ChatModel(val controller: ChatController) {
var currentUser = mutableStateOf<User?>(null)
@@ -32,8 +31,8 @@ enum class ChatType(val type: String) {
@Serializable
class User (
val userId: Int,
val userContactId: Int,
val userId: Long,
val userContactId: Long,
val localDisplayName: String,
val profile: Profile,
val activeUser: Boolean
@@ -54,21 +53,111 @@ class User (
typealias ChatId = String
interface NamedChat {
val displayName: String
val fullName: String
val chatViewName: String
get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName")
}
interface SomeChat {
val localDisplayName: String
val id: ChatId
val apiId: Long
val ready: Boolean
}
@Serializable
class Chat (
val chatInfo: ChatInfo,
val chatItems: List<ChatItem>,
val chatStats: ChatStats,
val serverInfo: ServerInfo = ServerInfo(NetworkStatus.Unknown())
) {
@Serializable
class ChatStats {
}
@Serializable
class ServerInfo(val networkStatus: NetworkStatus)
@Serializable
sealed class NetworkStatus {
abstract val statusString: String
abstract val statusExplanation: String
abstract val imageName: String
@Serializable
class Unknown: NetworkStatus() {
override val statusString get() = "Server connected"
override val statusExplanation get() = "You are connected to the server you use to receve messages from this contact."
override val imageName get() = "circle.dotted" // ?
}
}
}
@Serializable
sealed class ChatInfo: SomeChat, NamedChat {
@Serializable @SerialName("direct")
class Direct(val contact: Contact): ChatInfo() {
override val localDisplayName get() = contact.localDisplayName
override val id get() = contact.id
override val apiId get() = contact.apiId
override val ready get() = contact.ready
override val displayName get() = contact.displayName
override val fullName get() = contact.displayName
companion object {
val sampleData = Direct(Contact.sampleData)
}
}
@Serializable @SerialName("group")
class Group(val groupInfo: GroupInfo): ChatInfo() {
override val localDisplayName get() = groupInfo.localDisplayName
override val id get() = groupInfo.id
override val apiId get() = groupInfo.apiId
override val ready get() = groupInfo.ready
override val displayName get() = groupInfo.displayName
override val fullName get() = groupInfo.displayName
companion object {
val sampleData = Group(GroupInfo.sampleData)
}
}
@Serializable @SerialName("contactRequest")
class ContactRequest(val contactRequest: UserContactRequest): ChatInfo() {
override val localDisplayName get() = contactRequest.localDisplayName
override val id get() = contactRequest.id
override val apiId get() = contactRequest.apiId
override val ready get() = contactRequest.ready
override val displayName get() = contactRequest.displayName
override val fullName get() = contactRequest.displayName
companion object {
val sampleData = ContactRequest(UserContactRequest.sampleData)
}
}
}
@Serializable
class Contact(
val contactId: Int,
val localDisplayName: String,
val contactId: Long,
override val localDisplayName: String,
val profile: Profile,
val activeConn: Connection,
val viaGroup: Int? = null,
val viaGroup: Long? = null,
// no serializer for type Date?
// val createdAt: Date
): NamedChat {
val id: ChatId get() = "@$contactId"
val apiId: Int get() = contactId
val ready: Boolean get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready"
override val displayName: String get() = profile.displayName
override val fullName: String get() = profile.fullName
): SomeChat, NamedChat {
override val id get() = "@$contactId"
override val apiId get() = contactId
override val ready get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready"
override val displayName get() = profile.displayName
override val fullName get() = profile.fullName
companion object {
val sampleData = Contact(
@@ -101,9 +190,228 @@ class Profile(
}
}
interface NamedChat {
abstract val displayName: String
abstract val fullName: String
val chatViewName: String
get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName")
@Serializable
class GroupInfo (
val groupId: Long,
override val localDisplayName: String,
val groupProfile: GroupProfile,
// var createdAt: Date
): SomeChat, NamedChat {
override val id get() = "#$groupId"
override val apiId get() = groupId
override val ready get() = true
override val displayName get() = groupProfile.displayName
override val fullName get() = groupProfile.fullName
companion object {
val sampleData = GroupInfo(
groupId = 1,
localDisplayName = "team",
groupProfile = GroupProfile.sampleData,
// createdAt: Date()
)
}
}
@Serializable
class GroupProfile (
override val displayName: String,
override val fullName: String
): NamedChat {
companion object {
val sampleData = GroupProfile(
displayName = "team",
fullName = "My Team"
)
}
}
@Serializable
class GroupMember (
val groupMemberId: Long,
val memberId: String,
// var memberRole: GroupMemberRole
// var memberCategory: GroupMemberCategory
// var memberStatus: GroupMemberStatus
// var invitedBy: InvitedBy
val localDisplayName: String,
val memberProfile: Profile,
val memberContactId: Long?
// var activeConn: Connection?
) {
companion object {
val sampleData = GroupMember(
groupMemberId = 1,
memberId = "abcd",
localDisplayName = "alice",
memberProfile = Profile.sampleData,
memberContactId = 1
)
}
}
@Serializable
class UserContactRequest (
val contactRequestId: Long,
override val localDisplayName: String,
val profile: Profile
// val createdAt: Date
): SomeChat, NamedChat {
override val id get() = "<@$contactRequestId"
override val apiId get() = contactRequestId
override val ready get() = true
override val displayName get() = profile.displayName
override val fullName get() = profile.fullName
companion object {
val sampleData = UserContactRequest(
contactRequestId = 1,
localDisplayName = "alice",
profile = Profile.sampleData,
// createdAt: Date()
)
}
}
@Serializable
class AChatItem (
val chatInfo: ChatInfo,
val chatItem: ChatItem
)
@Serializable
class ChatItem (
val chatDir: CIDirection,
val meta: CIMeta,
val content: CIContent
) {
val id: Long get() = meta.itemId
// val timestampText: String get() = meta.timestampText
val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew
companion object {
fun getSampleData(id: Long, dir: CIDirection, ts: Date, text: String,status: CIStatus = CIStatus.SndNew()) =
ChatItem(
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text))
)
}
}
@Serializable
sealed class CIDirection {
abstract val sent: Boolean
@Serializable @SerialName("directSnd")
class DirectSnd: CIDirection() {
override val sent get() = true
}
@Serializable @SerialName("directRcv")
class DirectRcv: CIDirection() {
override val sent get() = false
}
@Serializable @SerialName("groupSnd")
class GroupSnd: CIDirection() {
override val sent get() = true
}
@Serializable @SerialName("groupRcv")
class GroupRcv(val groupMember: GroupMember): CIDirection() {
override val sent get() = false
}
}
@Serializable
class CIMeta (
val itemId: Long,
// val itemTs: Date,
val itemText: String,
val itemStatus: CIStatus,
// val createdAt: Date
) {
// val timestampText: String get() = getTimestampText(itemTs)
companion object {
fun getSample(id: Long, ts: Date, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta =
CIMeta(
itemId = id,
// itemTs = ts,
itemText = text,
itemStatus = status,
// createdAt = ts
)
}
}
// TODO
fun getTimestampText(d: Date): String = ""
@Serializable
sealed class CIStatus {
@Serializable @SerialName("sndNew")
class SndNew: CIStatus()
@Serializable @SerialName("sndSent")
class SndSent: CIStatus()
@Serializable @SerialName("sndErrorAuth")
class SndErrorAuth: CIStatus()
@Serializable @SerialName("sndError")
class SndError(val agentError: AgentErrorType): CIStatus()
@Serializable @SerialName("rcvNew")
class RcvNew: CIStatus()
@Serializable @SerialName("rcvRead")
class RcvRead: CIStatus()
}
@Serializable
sealed class CIContent {
abstract val text: String
@Serializable @SerialName("sndMsgContent")
class SndMsgContent(val msgContent: MsgContent): CIContent() {
override val text get() = msgContent.text
}
@Serializable @SerialName("rcvMsgContent")
class RcvMsgContent(val msgContent: MsgContent): CIContent() {
override val text get() = msgContent.text
}
@Serializable @SerialName("sndFileInvitation")
class SndFileInvitation(val fileId: Long, val filePath: String): CIContent() {
override val text get() = "sending files is not supported yet"
}
@Serializable @SerialName("rcvFileInvitation")
class RcvFileInvitation(val rcvFileTransfer: RcvFileTransfer): CIContent() {
override val text get() = "receiving files is not supported yet"
}
}
@Serializable
sealed class MsgContent {
abstract val text: String
abstract val cmdString: String
@Serializable @SerialName("text")
class MCText(override val text: String): MsgContent() {
override val cmdString get() = "text $text"
}
}
@Serializable
class RcvFileTransfer {
}
@Serializable
class AgentErrorType {
}

View File

@@ -46,9 +46,10 @@ open class ChatController(val ctrl: ChatCtrl) {
val c = cmd.cmdString
chatModel?.terminalItems?.add(TerminalItem.Cmd(System.currentTimeMillis(), cmd))
val json = chatSendCmd(ctrl, c)
Log.d("SIMPLEX", "sendCmd: $c")
Log.d("SIMPLEX", "sendCmd response $json")
Log.d("SIMPLEX", "sendCmd: ${cmd.cmdType}")
Log.d("SIMPLEX", "sendCmd response json $json")
val r = APIResponse.decodeStr(json)
Log.d("SIMPLEX", "sendCmd response type ${r.resp.responseType}")
chatModel?.terminalItems?.add(TerminalItem.Resp(System.currentTimeMillis(), r.resp))
r.resp
}
@@ -74,6 +75,12 @@ open class ChatController(val ctrl: ChatCtrl) {
throw Error("failed starting chat: ${r.toString()}")
}
suspend fun apiGetChats() {
val r = sendCmd(CC.ApiGetChats())
if (r is CR.ApiChats ) return
throw Error("failed getting the list of chats: ${r.toString()}")
}
class Mock: ChatController(0) {}
}
@@ -89,26 +96,36 @@ abstract class CC {
class ShowActiveUser: CC() {
override val cmdString get() = "/u"
override val cmdType get() = "ShowActiveUser"
override val cmdType get() = "showActiveUser"
}
class CreateActiveUser(val profile: Profile): CC() {
override val cmdString get() = "/u ${profile.displayName} ${profile.fullName}"
override val cmdType get() = "CreateActiveUser"
override val cmdType get() = "createActiveUser"
}
class StartChat: CC() {
override val cmdString get() = "/_start"
override val cmdType get() = "StartChat"
override val cmdType get() = "startChat"
}
class ApiGetChats: CC() {
override val cmdString get() = "/_get chats"
override val cmdType get() = "ApiGetChats"
override val cmdType get() = "apiGetChats"
}
class ApiGetChat(val type: ChatType, val id: Long): CC() {
override val cmdString get() = "/_get chat ${CC.chatRef(type, id)} count=100"
override val cmdType get() = "apiGetChat"
}
class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC() {
override val cmdString get() = "/_send ${CC.chatRef(type, id)} ${mc.cmdString}"
override val cmdType get() = "apiGetChat"
}
companion object {
fun chatRef(type: ChatType, id: String) = "${type}${id}"
fun chatRef(type: ChatType, id: Long) = "${type}${id}"
}
}
@@ -144,29 +161,49 @@ sealed class CR {
abstract val responseType: String
abstract val details: String
@Serializable
@SerialName("activeUser")
@Serializable @SerialName("activeUser")
class ActiveUser(val user: User): CR() {
override val responseType get() = "activeUser"
override val details get() = user.toString()
}
@Serializable
@SerialName("chatStarted")
@Serializable @SerialName("chatStarted")
class ChatStarted: CR() {
override val responseType get() = "chatStarted"
override val details get() = CR.noDetails(this)
}
@Serializable
@SerialName("contactSubscribed")
@Serializable @SerialName("apiChats")
class ApiChats(val chats: List<Chat>): CR() {
override val responseType get() = "apiChats"
override val details get() = chats.toString()
}
@Serializable @SerialName("apiChat")
class ApiChat(val chat: Chat): CR() {
override val responseType get() = "apiChats"
override val details get() = chat.toString()
}
@Serializable @SerialName("contactSubscribed")
class ContactSubscribed(val contact: Contact): CR() {
override val responseType get() = "contactSubscribed"
override val details get() = contact.toString()
}
@Serializable
@SerialName("cmdAccepted")
@Serializable @SerialName("newChatItem")
class NewChatItem(val chatItem: AChatItem): CR() {
override val responseType get() = "newChatItem"
override val details get() = chatItem.toString()
}
@Serializable @SerialName("chatItemUpdated")
class ChatItemUpdated(val chatItem: AChatItem): CR() {
override val responseType get() = "chatItemUpdated"
override val details get() = chatItem.toString()
}
@Serializable @SerialName("cmdAccepted")
class CmdAccepted(val corr: String): CR() {
override val responseType get() = "cmdAccepted"
override val details get() = "corrId: $corr"

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material.Button
@@ -60,9 +61,11 @@ fun DetailView(identifier: Long, terminalItems: List<TerminalItem>, navControlle
Column(
modifier=Modifier.verticalScroll(rememberScrollState())
) {
Text((terminalItems.filter {it.id == identifier}).first().details)
Button(onClick = { navController.popBackStack() }) {
Text("Back")
}
SelectionContainer {
Text((terminalItems.filter { it.id == identifier }).first().details)
}
}
}

View File

@@ -0,0 +1,10 @@
package chat.simplex.app.views.chat
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import chat.simplex.app.model.Chat
@Composable
fun ChatView(chat: Chat) {
Text("ChatView")
}

View File

@@ -0,0 +1,10 @@
package chat.simplex.app.views.chat.item
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import chat.simplex.app.model.ChatItem
@Composable
fun ChatItemView(chatItem: ChatItem) {
Text("ChatItemView")
}

View File

@@ -0,0 +1,10 @@
package chat.simplex.app.views.chat.item
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import chat.simplex.app.model.ChatItem
@Composable
fun TextItemView(chatItem: ChatItem) {
Text("TextItemView")
}

View File

@@ -0,0 +1,10 @@
package chat.simplex.app.views.chatlist
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import chat.simplex.app.model.ChatModel
@Composable
fun ChatListView(chatModel: ChatModel) {
Text("ChatListView")
}

View File

@@ -0,0 +1,10 @@
package chat.simplex.app.views.chatlist
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import chat.simplex.app.model.Chat
@Composable
fun ChatPreviewView(chat: Chat) {
Text("ChatPreviewView")
}