diff --git a/apps/android/.gitignore b/apps/android/.gitignore index aa724b7707..4d1f29a2d1 100644 --- a/apps/android/.gitignore +++ b/apps/android/.gitignore @@ -7,6 +7,8 @@ /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml +/.idea/deploymentTargetDropDown.xml +/.idea/misc.xml .DS_Store /build /captures diff --git a/apps/android/.idea/misc.xml b/apps/android/.idea/misc.xml deleted file mode 100644 index f0d8957869..0000000000 --- a/apps/android/.idea/misc.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - \ No newline at end of file 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 62a4113a1e..8adeceecf6 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 @@ -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") 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 5c82f51225..f8a7082595 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 @@ -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(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, + 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 { + } 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 974e6d1776..bca01a174d 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 @@ -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): 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" 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 4fde8eb87c..bc7705f5e6 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 @@ -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, 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) + } } } 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 new file mode 100644 index 0000000000..4efa2c50d0 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -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") +} 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 new file mode 100644 index 0000000000..10fc3c9f9c --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -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") +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt new file mode 100644 index 0000000000..22bbdbb005 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -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") +} 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 new file mode 100644 index 0000000000..634af6248d --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -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") +} 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 new file mode 100644 index 0000000000..993e9b96a5 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -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") +}