started android / compose app (#301)

* new compose project

* classes for chat command and response

* use val with get() for commands and responses

* chat model

* initial jetpack compose set up

* wire it up with chat

* first ability to send and receive messages

* refactor model/controller interface

* JSON samples

* terminal view with items

* playing around with json

* JSON serialization works

* parsing API responses in the terminal

* add subclass for contactSubscribed reponse

* remove android-poc

* remove JSON example

Co-authored-by: IanRDavies <ian_davies_@hotmail.co.uk>
This commit is contained in:
Evgeny Poberezkin 2022-02-16 12:49:47 +00:00 committed by GitHub
parent 322ab9d854
commit ce02c514cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 665 additions and 352 deletions

View file

@ -8,17 +8,8 @@
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build/
release/
debug/
/build
/captures
.externalNativeBuild
.cxx
local.properties
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

3
apps/android/.idea/.gitignore generated vendored
View file

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View file

@ -1,117 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_4a_API_32.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-02-15T15:32:14.669079Z" />
</component>
</project>

View file

@ -0,0 +1,20 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View file

@ -3,9 +3,10 @@
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.2328125" />
<entry key="app/src/main/res/drawable/ic_launcher_background.xml" value="0.2328125" />
<entry key="app/src/main/res/layout/activity_main.xml" value="1.0" />
<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" />
</map>
</option>
</component>

1
apps/android/app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
@ -17,6 +18,9 @@ android {
ndk {
abiFilters 'arm64-v8a'
}
vectorDrawables {
useSupportLibrary true
}
externalNativeBuild {
cmake {
cppFlags ''
@ -44,17 +48,29 @@ android {
}
}
buildFeatures {
viewBinding true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.+'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}

View file

@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name="SimplexApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@ -15,7 +16,9 @@
android:theme="@style/Theme.SimpleX">
<activity
android:name=".MainActivity"
android:exported="true">
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SimpleX">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -8,7 +8,7 @@ void setLineBuffering(void);
int pipe_std_to_socket(const char * name);
JNIEXPORT jint JNICALL
Java_chat_simplex_app_MainActivityKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) {
Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) {
const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE);
int ret = pipe_std_to_socket(name);
(*env)->ReleaseStringUTFChars(env, socket_name, name);
@ -16,7 +16,7 @@ Java_chat_simplex_app_MainActivityKt_pipeStdOutToSocket(JNIEnv *env, __unused jc
}
JNIEXPORT void JNICALL
Java_chat_simplex_app_MainActivityKt_initHS(__unused JNIEnv *env, __unused jclass clazz) {
Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass clazz) {
hs_init(NULL, NULL);
setLineBuffering();
}
@ -33,7 +33,7 @@ extern char *chat_send_cmd(controller ctl, const char *cmd);
extern char *chat_recv_msg(controller ctl);
JNIEXPORT jlong JNICALL
Java_chat_simplex_app_MainActivityKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) {
Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) {
const char *_data = (*env)->GetStringUTFChars(env, datadir, JNI_FALSE);
jlong res = (jlong)chat_init_store(_data);
(*env)->ReleaseStringUTFChars(env, datadir, _data);
@ -41,12 +41,12 @@ Java_chat_simplex_app_MainActivityKt_chatInit(JNIEnv *env, __unused jclass clazz
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_app_MainActivityKt_chatGetUser(JNIEnv *env, __unused jclass clazz, jlong controller) {
Java_chat_simplex_app_SimplexAppKt_chatGetUser(JNIEnv *env, __unused jclass clazz, jlong controller) {
return (*env)->NewStringUTF(env, chat_get_user((void*)controller));
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_app_MainActivityKt_chatCreateUser(JNIEnv *env, __unused jclass clazz, jlong controller, jstring data) {
Java_chat_simplex_app_SimplexAppKt_chatCreateUser(JNIEnv *env, __unused jclass clazz, jlong controller, jstring data) {
const char *_data = (*env)->GetStringUTFChars(env, data, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_create_user((void*)controller, _data));
(*env)->ReleaseStringUTFChars(env, data, _data);
@ -54,12 +54,12 @@ Java_chat_simplex_app_MainActivityKt_chatCreateUser(JNIEnv *env, __unused jclass
}
JNIEXPORT jlong JNICALL
Java_chat_simplex_app_MainActivityKt_chatStart(JNIEnv *env, jclass clazz, jlong controller) {
Java_chat_simplex_app_SimplexAppKt_chatStart(JNIEnv *env, jclass clazz, jlong controller) {
return (jlong)chat_start((void*)controller);
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_app_MainActivityKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) {
Java_chat_simplex_app_SimplexAppKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) {
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
(*env)->ReleaseStringUTFChars(env, msg, _msg);
@ -67,6 +67,6 @@ Java_chat_simplex_app_MainActivityKt_chatSendCmd(JNIEnv *env, __unused jclass cl
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_app_MainActivityKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) {
Java_chat_simplex_app_SimplexAppKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) {
return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller));
}

View file

@ -1,123 +1,37 @@
package chat.simplex.app
import android.net.LocalServerSocket
import android.app.Application
import android.os.Bundle
import android.util.Log
import android.view.inputmethod.EditorInfo
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatEditText
import java.io.BufferedReader
import java.io.InputStreamReader
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.Semaphore
import kotlin.concurrent.thread
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import chat.simplex.app.ui.theme.SimpleXTheme
import androidx.lifecycle.AndroidViewModel
import chat.simplex.app.model.*
import chat.simplex.app.views.TerminalView
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
// ghc's rts
external fun initHS()
// android-support
external fun pipeStdOutToSocket(socketName: String) : Int
// simplex-chat
typealias Store = Long
typealias Controller = Long
external fun chatInit(filesDir: String): Store
external fun chatGetUser(controller: Store) : String
external fun chatCreateUser(controller: Store, data: String) : String
external fun chatStart(controller: Store) : Controller
external fun chatSendCmd(controller: Controller, msg: String) : String
external fun chatRecvMsg(controller: Controller) : String
class MainActivity : AppCompatActivity() {
class MainActivity: ComponentActivity() {
private val viewModel by viewModels<SimplexViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
weakActivity = WeakReference(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val store : Store = chatInit(this.applicationContext.filesDir.toString())
// create user if needed
if(chatGetUser(store) == "{}") {
chatCreateUser(store, """
{"displayName": "test", "fullName": "android test"}
""".trimIndent())
}
Log.d("SIMPLEX (user)", chatGetUser(store))
val controller = chatStart(store)
val cmdinput = this.findViewById<AppCompatEditText>(R.id.cmdInput)
cmdinput.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_SEND -> {
Log.d("SIMPLEX SEND", chatSendCmd(controller, cmdinput.text.toString()))
cmdinput.text?.clear()
true
}
else -> false
}
}
thread(name="receiver") {
val chatlog = FifoQueue<String>(500)
while(true) {
val msg = chatRecvMsg(controller)
Log.d("SIMPLEX RECV", msg)
chatlog.add(msg)
val currentText = chatlog.joinToString("\n")
weakActivity.get()?.runOnUiThread {
val log = weakActivity.get()?.findViewById<TextView>(R.id.chatlog)
val scroll = weakActivity.get()?.findViewById<ScrollView>(R.id.scroller)
log?.text = currentText
scroll?.scrollTo(0, scroll.getChildAt(0).height)
setContent {
SimpleXTheme {
MainPage(viewModel)
}
}
}
}
companion object {
lateinit var weakActivity : WeakReference<MainActivity>
init {
val socketName = "local.socket.address.listen.native.cmd2"
val s = Semaphore(0)
thread(name="stdout/stderr pipe") {
Log.d("SIMPLEX", "starting server")
val server = LocalServerSocket(socketName)
Log.d("SIMPLEX", "started server")
s.release()
val receiver = server.accept()
Log.d("SIMPLEX", "started receiver")
val logbuffer = FifoQueue<String>(500)
if (receiver != null) {
val inStream = receiver.inputStream
val inStreamReader = InputStreamReader(inStream)
val input = BufferedReader(inStreamReader)
while(true) {
val line = input.readLine() ?: break
Log.d("SIMPLEX (stdout/stderr)", line)
logbuffer.add(line)
}
}
class SimplexViewModel(application: Application) : AndroidViewModel(application) {
val chatModel = getApplication<SimplexApp>().chatModel
}
System.loadLibrary("app-lib")
s.acquire()
pipeStdOutToSocket(socketName)
initHS()
}
}
}
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
override fun add(element: E): Boolean {
if(size > capacity) removeFirst()
return super.add(element)
}
@Composable
fun MainPage(vm: SimplexViewModel) {
TerminalView(vm.chatModel)
}

View file

@ -0,0 +1,95 @@
package chat.simplex.app
import android.app.Application
import android.net.LocalServerSocket
import android.util.Log
import chat.simplex.app.model.ChatController
import chat.simplex.app.model.ChatModel
import java.io.BufferedReader
import java.io.InputStreamReader
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.Semaphore
import kotlin.concurrent.thread
// ghc's rts
external fun initHS()
// android-support
external fun pipeStdOutToSocket(socketName: String) : Int
// SimpleX API
typealias Controller = Long
typealias Store = Long
external fun chatInit(filesDir: String): Store
external fun chatGetUser(controller: Store) : String
external fun chatCreateUser(controller: Store, data: String) : String
external fun chatStart(controller: Store) : Controller
external fun chatSendCmd(controller: Controller, msg: String) : String
external fun chatRecvMsg(controller: Controller) : String
class SimplexApp: Application() {
private lateinit var controller: ChatController
override fun onCreate() {
super.onCreate()
val store: Store = chatInit(applicationContext.filesDir.toString())
// create user if needed
if (chatGetUser(store) == "{}") {
chatCreateUser(store, """
{"displayName": "test", "fullName": "android test"}
""".trimIndent())
}
Log.d("SIMPLEX (user)", chatGetUser(store))
controller = ChatController(chatStart(store))
}
val chatModel by lazy {
val m = ChatModel(controller)
controller.setModel(m)
controller.startReceiver()
m
}
companion object {
lateinit var weakActivity: WeakReference<MainActivity>
init {
val socketName = "local.socket.address.listen.native.cmd2"
val s = Semaphore(0)
thread(name="stdout/stderr pipe") {
Log.d("SIMPLEX", "starting server")
val server = LocalServerSocket(socketName)
Log.d("SIMPLEX", "started server")
s.release()
val receiver = server.accept()
Log.d("SIMPLEX", "started receiver")
val logbuffer = FifoQueue<String>(500)
if (receiver != null) {
val inStream = receiver.inputStream
val inStreamReader = InputStreamReader(inStream)
val input = BufferedReader(inStreamReader)
while(true) {
val line = input.readLine() ?: break
Log.d("SIMPLEX (stdout/stderr)", line)
logbuffer.add(line)
}
}
}
System.loadLibrary("app-lib")
s.acquire()
pipeStdOutToSocket(socketName)
initHS()
}
}
}
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
override fun add(element: E): Boolean {
if(size > capacity) removeFirst()
return super.add(element)
}
}

View file

@ -0,0 +1,105 @@
package chat.simplex.app.model
import androidx.compose.runtime.mutableStateListOf
import kotlinx.serialization.Serializable
import java.util.*
class ChatModel(val controller: ChatController) {
val currentUser: User? = null
var terminalItems = mutableStateListOf<TerminalItem>()
companion object {
val sampleData: ChatModel get() {
val m = ChatModel(ChatController.Mock())
m.terminalItems = mutableStateListOf(
TerminalItem.Cmd(CC.ShowActiveUser()),
TerminalItem.Resp(CR.ActiveUser(User.sampleData))
)
return m
}
}
}
enum class ChatType(val type: String) {
Direct("@"),
Group("#"),
ContactRequest("<@")
}
@Serializable
class User (
val userId: Int,
val userContactId: Int,
val localDisplayName: String,
val profile: Profile,
val activeUser: Boolean
) : NamedChat {
override val displayName: String get() = profile.displayName
override val fullName: String get() = profile.fullName
companion object {
val sampleData = User(
userId = 1,
userContactId = 1,
localDisplayName = "alice",
profile = Profile.sampleData,
activeUser = true
)
}
}
typealias ChatId = String
@Serializable
class Contact(
val contactId: Int,
val localDisplayName: String,
val profile: Profile,
val activeConn: Connection,
val viaGroup: Int? = 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
companion object {
val sampleData = Contact(
contactId = 1,
localDisplayName = "alice",
profile = Profile.sampleData,
activeConn = Connection.sampleData
// createdAt = Date()
)
}
}
@Serializable
class Connection(val connStatus: String) {
companion object {
val sampleData = Connection(connStatus = "ready")
}
}
@Serializable
class Profile(
val displayName: String,
val fullName: String
) {
companion object {
val sampleData = Profile(
displayName = "alice",
fullName = "Alice"
)
}
}
interface NamedChat {
abstract val displayName: String
abstract val fullName: String
val chatViewName: String
get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName")
}

View file

@ -0,0 +1,161 @@
package chat.simplex.app.model
import android.util.Log
import chat.simplex.app.chatRecvMsg
import chat.simplex.app.chatSendCmd
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.lang.Exception
import java.util.*
import kotlin.concurrent.thread
typealias Controller = Long
open class ChatController(val ctrl: Controller) {
private lateinit var chatModel: ChatModel
fun setModel(m: ChatModel) {
chatModel = m
}
fun startReceiver() {
thread(name="receiver") {
// val chatlog = FifoQueue<String>(500)
while(true) {
val json = chatRecvMsg(ctrl)
Log.d("SIMPLEX chatRecvMsg: ", json)
chatModel.terminalItems.add(TerminalItem.Resp(APIResponse.decodeStr(json)))
}
}
}
fun sendCmd(cmd: String) {
val json = chatSendCmd(ctrl, cmd)
Log.d("SIMPLEX chatSendCmd: ", cmd)
Log.d("SIMPLEX chatSendCmd response: ", json)
chatModel.terminalItems.add(TerminalItem.Resp(APIResponse.decodeStr(json)))
}
class Mock: ChatController(0) {}
}
// ChatCommand
abstract class CC {
abstract val cmdString: String
abstract val cmdType: String
class Console(val cmd: String): CC() {
override val cmdString get() = cmd
override val cmdType get() = "console command"
}
class ShowActiveUser: CC() {
override val cmdString get() = "/u"
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"
}
class StartChat: CC() {
override val cmdString get() = "/_start"
override val cmdType get() = "StartChat"
}
class ApiGetChats: CC() {
override val cmdString get() = "/_get chats"
override val cmdType get() = "ApiGetChats"
}
companion object {
fun chatRef(type: ChatType, id: String) = "${type}${id}"
}
}
val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
}
@Serializable
class APIResponse(val resp: CR) {
companion object {
fun decodeStr(str: String): CR {
try {
return json.decodeFromString<APIResponse>(str).resp
} catch(e: Exception) {
try {
val data = json.parseToJsonElement(str)
return CR.Response(data.jsonObject["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data))
} catch(e: Exception) {
return CR.Invalid(str)
}
}
}
}
}
// ChatResponse
@Serializable
sealed class CR {
abstract val responseType: String
abstract val details: String
@Serializable
@SerialName("activeUser")
class ActiveUser(val user: User): CR() {
override val responseType get() = "activeUser"
override val details get() = user.toString()
}
@Serializable
@SerialName("contactSubscribed")
class ContactSubscribed(val contact: Contact): CR() {
override val responseType get() = "contactSubscribed"
override val details get() = contact.toString()
}
@Serializable
class Response(val type: String, val json: String): CR() {
override val responseType get() = "* ${type}"
override val details get() = json
}
@Serializable
class Invalid(val str: String): CR() {
override val responseType get() = "* invalid json"
override val details get() = str
}
// {"resp": {"activeUser": {"user": {<user>}}}}
// {"resp": {"anythingElse": <json> }} -> Unknown(type = "anythingElse", json = "<the whole thing including resp>")
// {"type": "activeUser", "user": <user>}
}
abstract class TerminalItem {
val date = Date()
abstract val label: String
abstract val details: String
class Cmd(val cmd: CC): TerminalItem() {
override val label get() = "> ${cmd.cmdString.substring(0, 30)}"
override val details get() = cmd.cmdString
}
class Resp(val resp: CR): TerminalItem() {
override val label get() = "< ${resp.responseType}"
override val details get() = resp.details
}
companion object {
val sampleData = listOf<TerminalItem>(
TerminalItem.Cmd(CC.ShowActiveUser()),
TerminalItem.Resp(CR.ActiveUser(User.sampleData))
)
}
}

View file

@ -0,0 +1,8 @@
package chat.simplex.app.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

View file

@ -0,0 +1,11 @@
package chat.simplex.app.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View file

@ -0,0 +1,44 @@
package chat.simplex.app.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun SimpleXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

View file

@ -0,0 +1,28 @@
package chat.simplex.app.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)

View file

@ -0,0 +1,38 @@
package chat.simplex.app.views
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.TerminalItem
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.SendMsgView
@Composable
fun TerminalView(chatModel: ChatModel) {
Column {
TerminalLog(chatModel.terminalItems)
SendMsgView(chatModel.controller::sendCmd)
}
}
@Composable
fun TerminalLog(terminalItems: List<TerminalItem>) {
LazyColumn {
items(terminalItems) { item ->
Text(item.label)
}
}
}
@Preview
@Composable
fun PreviewSendMsgView() {
SimpleXTheme {
TerminalView(chatModel = ChatModel.sampleData)
}
}

View file

@ -0,0 +1,40 @@
package chat.simplex.app.views.chat
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable
fun SendMsgView (sendMessage: (String) -> Unit) {
var cmd by remember { mutableStateOf("") }
Row {
TextField(value = cmd, onValueChange = { cmd = it }, modifier = Modifier.height(60.dp))
Spacer(Modifier.height(10.dp))
Button(
onClick = {
sendMessage(cmd)
cmd = ""
},
modifier = Modifier.width(40.dp).height(60.dp),
enabled = cmd.isNotEmpty()
) {
Text("Go")
}
}
}
@Preview
@Composable
fun PreviewSendMsgView() {
SimpleXTheme {
SendMsgView(
sendMessage = { msg -> println(msg) }
)
}
}

View file

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/cmdInput"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:autofillHints=""
android:ems="10"
android:imeOptions="actionSend"
android:inputType="textPersonName"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:ignore="SpeakableTextPresentCheck" />
<ScrollView
android:id="@+id/scroller"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/cmdInput"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/chatlog"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.SimpleX" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -1,16 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.SimpleX" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.SimpleX" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/purple_700</item>
</style>
</resources>

View file

@ -1,16 +1,25 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
compose_version = '1.1.0'
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.4"
classpath 'com.android.tools.build:gradle:7.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
classpath "org.jetbrains.kotlin:kotlin-serialization:1.3.2"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.1.1' apply false
id 'com.android.library' version '7.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
}
task clean(type: Delete) {

View file

@ -15,7 +15,11 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true

Binary file not shown.

View file

@ -1,6 +1,6 @@
#Fri Jan 21 23:13:54 GMT 2022
#Mon Feb 14 14:23:51 GMT 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View file

@ -1,9 +1,15 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
}
}
rootProject.name = "SimpleX"