From 6122c1842792e2a5612eef2260fbf43bc2ade5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20K=C3=BCrten?= <sebastian@mobanisto.de> Date: Thu, 9 Sep 2021 22:46:49 +0200 Subject: [PATCH] Add UI stubs from Paul's prototype --- build.gradle.kts | 1 + .../briar/desktop/BriarService.kt | 8 +- .../briar/desktop/paul/data/Contacts.kt | 229 +++++++++++++ .../briar/desktop/paul/data/ContentType.kt | 16 + .../briar/desktop/paul/data/OptionType.kt | 14 + .../briar/desktop/paul/model/Contact.kt | 9 + .../briar/desktop/paul/model/ContentType.kt | 7 + .../briar/desktop/paul/model/Message.kt | 9 + .../briar/desktop/paul/model/OptionType.kt | 9 + .../briar/desktop/paul/theme/colors.kt | 32 ++ .../briar/desktop/paul/views/BriarSidebar.kt | 103 ++++++ .../desktop/paul/views/BriarUIStateManager.kt | 51 +++ .../desktop/paul/views/PrivateMessageView.kt | 319 ++++++++++++++++++ .../resources/images/profile_images/p0.png | Bin 0 -> 45426 bytes .../resources/images/profile_images/p1.png | Bin 0 -> 2804 bytes .../resources/images/profile_images/p2.png | Bin 0 -> 45426 bytes .../resources/images/profile_images/p3.png | Bin 0 -> 45426 bytes .../resources/images/profile_images/p4.png | Bin 0 -> 45426 bytes .../resources/images/profile_images/p5.png | Bin 0 -> 45426 bytes 19 files changed, 800 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/data/Contacts.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentType.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionType.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/model/Contact.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/model/Message.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/model/OptionType.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/theme/colors.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt create mode 100644 src/main/resources/images/profile_images/p0.png create mode 100644 src/main/resources/images/profile_images/p1.png create mode 100644 src/main/resources/images/profile_images/p2.png create mode 100644 src/main/resources/images/profile_images/p3.png create mode 100644 src/main/resources/images/profile_images/p4.png create mode 100644 src/main/resources/images/profile_images/p5.png diff --git a/build.gradle.kts b/build.gradle.kts index 66f5b38406..af4aeb877c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.10.0") implementation("com.github.ajalt:clikt:2.2.0") + implementation("org.jetbrains.compose.material:material-icons-extended:0.4.0") implementation(project(path = ":briar:briar-core", configuration = "default")) implementation(project(path = ":briar:bramble-java", configuration = "default")) diff --git a/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt b/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt index ad43f0bb61..18a11ca523 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt @@ -66,13 +66,7 @@ constructor( }) is Screen.Main -> - Column( - modifier = Modifier.padding(16.dp).fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Welcome to Briar") - } + briarUIStateManager() } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/Contacts.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/Contacts.kt new file mode 100644 index 0000000000..2b7b47c203 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/Contacts.kt @@ -0,0 +1,229 @@ +package org.briarproject.briar.desktop.paul.data + +import org.briarproject.briar.desktop.paul.model.Contact +import org.briarproject.briar.desktop.paul.model.Message + +object ContactList { + val msg1: List<Message> = listOf<Message>( + Message(from = "Alice", message = "hello!", time = "2 days ago", delivered = true), + Message(from = null, message = "yes I hear you", time = "1 day ago", delivered = true), + Message( + from = null, + message = "I am messaging you through this fake Briar Desktop app", + time = "18 hrs. ago", + delivered = true + ), + Message(from = "Alice", message = "Ah I see, very neat", time = "12 min. ago", delivered = true), + Message(from = null, message = "Loren Ipsum", time = "2 min. ago", delivered = false) + ) + val msg2: List<Message> = listOf<Message>( + Message( + from = null, + message = "The air bites shrewdly; it is very cold.", + time = "2 days ago", + delivered = true + ), + Message(from = "Bob", message = "It is a nipping and an eager air.", time = "1 day ago", delivered = true), + Message(from = null, message = "What hour now?", time = "18 hrs. ago", delivered = true), + Message(from = "Bob", message = "I think it lacks of twelve.", time = "12 min. ago", delivered = true), + Message(from = null, message = "No, it is struck.", time = "2 min. ago", delivered = false), + Message( + from = "Bob", + message = "Indeed? I heard it not: then it draws near the season Wherein the spirit held his wont to walk. A flourish of trumpets, and ordnance shot off, within. What does this mean, my lord?", + time = "2 min. ago", + delivered = false + ), + Message( + from = null, + message = "The air bites shrewdly; it is very cold.", + time = "2 days ago", + delivered = true + ), + Message(from = "Bob", message = "It is a nipping and an eager air.", time = "1 day ago", delivered = true), + Message(from = null, message = "What hour now?", time = "18 hrs. ago", delivered = true), + Message(from = "Bob", message = "I think it lacks of twelve.", time = "12 min. ago", delivered = true), + Message(from = null, message = "No, it is struck.", time = "2 min. ago", delivered = false), + Message( + from = null, + message = "The air bites shrewdly; it is very cold.", + time = "2 days ago", + delivered = true + ), + Message( + from = null, + message = "The air bites shrewdly; it is very cold.", + time = "2 days ago", + delivered = true + ), + Message(from = "Bob", message = "It is a nipping and an eager air.", time = "1 day ago", delivered = true), + Message(from = null, message = "What hour now?", time = "18 hrs. ago", delivered = true), + Message(from = "Bob", message = "I think it lacks of twelve.", time = "12 min. ago", delivered = true), + Message(from = null, message = "No, it is struck.", time = "2 min. ago", delivered = false), + Message(from = "Bob", message = "It is a nipping and an eager air.", time = "1 day ago", delivered = true), + Message(from = null, message = "What hour now?", time = "18 hrs. ago", delivered = true), + Message(from = "Bob", message = "I think it lacks of twelve.", time = "12 min. ago", delivered = true), + Message(from = null, message = "No, it is struck.", time = "2 min. ago", delivered = false), + Message( + from = "Bob", + message = "Indeed? I heard it not: then it draws near the season Wherein the spirit held his wont to walk. A flourish of trumpets, and ordnance shot off, within. What does this mean, my lord?", + time = "2 min. ago", + delivered = false + ) + ) + val msg3: List<Message> = listOf<Message>( + Message( + from = null, + message = "Give him this money and these notes, Reynaldo.", + time = "2 days ago", + delivered = true + ), + Message(from = "Bob", message = "I will, my lord.", time = "1 day ago", delivered = true), + Message( + from = null, + message = "You shall do marvellous wisely, good Reynaldo,Before you visit him, to make inquireOf his behavior.", + time = "18 hrs. ago", + delivered = true + ), + Message(from = "Bob", message = "My lord, I did intend it.", time = "12 min. ago", delivered = true), + Message( + from = null, + message = "Marry, well said; very well said. Look you, sir,Inquire me first what Danskers are in Paris;And how, and who, what means, and where they keep,What company, at what expense; and findingBy this encompassment and drift of questionThat they do know my son, come you more nearerThan your particular demands will touch it:Take you, as ’twere, some distant knowledge of him;As thus, ‘I know his father and his friends,And in part him: ‘ do you mark this, Reynaldo?", + time = "2 min. ago", + delivered = false + ), + ) + val msg4: List<Message> = listOf<Message>( + Message( + from = "Bob", + message = "So art thou to revenge, when thou shalt hear.", + time = "2 days ago", + delivered = true + ), + Message(from = null, message = "What?", time = "1 day ago", delivered = true), + Message( + from = "Bob", + message = "I am your father’s spirit, doomed for a certain time to walk the night, and for the day to burn in fires, till the foul crimes done during my lifetime have been burnt and purged away. But that I am forbidden to tell the secrets of my prison-house I could tell a tale whose lightest word would shrivel up your soul, freeze your young blood, make your eyes start from their sockets and your hair stand up on end like the quills of a frightened porcupine. But this eternal torture is not for ears of flesh and blood. Listen, oh listen! If you ever loved your dear father ….", + time = "18 hrs. ago", + delivered = true + ), + Message(from = null, message = "Oh God!", time = "12 min. ago", delivered = true), + ) + val msg5: List<Message> = listOf<Message>( + Message( + from = null, + message = "Here's a knocking indeed! If a man were porter of hell-gate, he should have old turning the key. Knocking within Knock, knock, knock! Who's there, i' the name of Beelzebub? Here's a farmer, that hanged himself on the expectation of plenty: come in time; have napkins enow about you; here you'll sweat for't.", + time = "2 days ago", + delivered = true + ), + Message( + from = null, + message = "Knock, knock! Who's there, in the other devil's name? Faith, here's an equivocator, that could swear in both the scales against either scale; who committed treason enough for God's sake, yet could not equivocate to heaven: O, come in, equivocator.", + time = "1 day ago", + delivered = true + ), + ) + val contacts = listOf( + Contact(name = "Alice", online = true, profile_pic = "p1.png", last_heard = "now", privateMessages = msg2), + Contact( + name = "Bob", + online = false, + profile_pic = "p2.png", + last_heard = "22 min. ago", + privateMessages = msg1 + ), + Contact( + name = "Carl", + online = true, + profile_pic = "p3.png", + last_heard = "2 hr. ago ", + privateMessages = msg3 + ), + Contact(name = "Dan", online = false, profile_pic = "p4.png", last_heard = "1 day ago", privateMessages = msg4), + Contact( + name = "Eve", + online = false, + profile_pic = "p5.png", + last_heard = "3 days ago", + privateMessages = msg5 + ), + Contact( + name = "Fred", + online = false, + profile_pic = "p2.png", + last_heard = "22 min. ago", + privateMessages = msg1 + ), + Contact( + name = "Greg", + online = true, + profile_pic = "p3.png", + last_heard = "2 hr. ago ", + privateMessages = msg3 + ), + Contact( + name = "Harold", + online = false, + profile_pic = "p4.png", + last_heard = "1 day ago", + privateMessages = msg4 + ), + Contact( + name = "Irene", + online = false, + profile_pic = "p5.png", + last_heard = "3 days ago", + privateMessages = msg5 + ), + Contact( + name = "Jeanne", + online = false, + profile_pic = "p2.png", + last_heard = "22 min. ago", + privateMessages = msg1 + ), + Contact( + name = "Karl", + online = true, + profile_pic = "p3.png", + last_heard = "2 hr. ago ", + privateMessages = msg3 + ), + Contact( + name = "Lorn", + online = false, + profile_pic = "p4.png", + last_heard = "1 day ago", + privateMessages = msg4 + ), + Contact( + name = "Meg", + online = false, + profile_pic = "p5.png", + last_heard = "3 days ago", + privateMessages = msg5 + ), + Contact( + name = "Nile", + online = false, + profile_pic = "p2.png", + last_heard = "22 min. ago", + privateMessages = msg1 + ), + Contact( + name = "Oscar", + online = true, + profile_pic = "p3.png", + last_heard = "2 hr. ago ", + privateMessages = msg3 + ), + Contact( + name = "Paul", + online = false, + profile_pic = "p4.png", + last_heard = "1 day ago", + privateMessages = msg4 + ), + Contact(name = "Qi", online = false, profile_pic = "p5.png", last_heard = "3 days ago", privateMessages = msg5), + ) +} + diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentType.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentType.kt new file mode 100644 index 0000000000..e29adb5f0b --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentType.kt @@ -0,0 +1,16 @@ +package org.briarproject.briar.desktop.paul.data + +import org.briarproject.briar.desktop.paul.model.ContentType + +object ContentTypeList { + val types = listOf( + ContentType(id = -1, name = "Profile"), + ContentType(id = 0, name = "Contacts"), + ContentType(id = 1, name = "Private Groups"), + ContentType(id = 2, name = "Forums"), + ContentType(id = 3, name = "Blogs"), + ContentType(id = 4, name = "Transports"), + ContentType(id = 5, name = "Settings"), + ContentType(id = 6, name = "Sign Out") + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionType.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionType.kt new file mode 100644 index 0000000000..5dbb5d8e4c --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionType.kt @@ -0,0 +1,14 @@ +package org.briarproject.briar.desktop.paul.data + +import org.briarproject.briar.desktop.paul.model.OptionType + +object OptionTypeList { + val msgTypes = listOf( + OptionType(name = "Alice", unread = 2, online = true), + OptionType(name = "Bob", unread = 0, online = true), + OptionType(name = "Carl", unread = 0, online = false), + OptionType(name = "Dan", unread = 1, online = false), + OptionType(name = "Eve", unread = 0, online = false), + ) +} + diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/Contact.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/Contact.kt new file mode 100644 index 0000000000..22085dff5b --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/Contact.kt @@ -0,0 +1,9 @@ +package org.briarproject.briar.desktop.paul.model + +data class Contact( + val name: String, + val online: Boolean, + val profile_pic: String, + val last_heard: String, + val privateMessages: List<Message> +) \ No newline at end of file diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt new file mode 100644 index 0000000000..0db88d3071 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt @@ -0,0 +1,7 @@ +package org.briarproject.briar.desktop.paul.model + +data class ContentType( + val id: Int, + val name: String, + //val icon: Icon, +) \ No newline at end of file diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/Message.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/Message.kt new file mode 100644 index 0000000000..a7a79f553f --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/Message.kt @@ -0,0 +1,9 @@ +package org.briarproject.briar.desktop.paul.model + +data class Message( + val from: String?, + val message: String, + val time: String, + val delivered: Boolean, + //val read: Boolean, +) \ No newline at end of file diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/OptionType.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/OptionType.kt new file mode 100644 index 0000000000..8367e1eb87 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/OptionType.kt @@ -0,0 +1,9 @@ +package org.briarproject.briar.desktop.paul.model + +data class OptionType( + val name: String, + val unread: Int, + val online: Boolean + //val icon: Icon, +) + diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/colors.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/colors.kt new file mode 100644 index 0000000000..2d443639ee --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/colors.kt @@ -0,0 +1,32 @@ +package org.briarproject.briar.desktop.paul.theme + +import androidx.compose.material.darkColors +import androidx.compose.ui.graphics.Color + +val graySurface = Color(0xFF2A2A2A) +val lightGray = Color(0xFFD3D3D3) +val briarGray = Color(0xff222222) +val briarBlack = Color(0xff1E2228) +val briarSelBlack = Color(0xff495261) +val briarDarkGray = Color(0xFF3D4552) +val darkGray = Color(0xFF151515) +val divider = Color(0xff35383D) +val briarBlue = Color(0xFF2D3E50) +val briarLightBlue = Color(0xFFEBEFF2) +val briarGreen = Color(0xFF97D323) +val briarBlueMsg = Color(0xFF1b69b6) +val briarBlueSpecialMsg = Color(0xFF134a80) +val briarGrayMsg = Color(0xff3b4047) +val briarGraySpecialMsg = Color(0xFF212d3b) + +val DarkColorPallet = darkColors( + primary = Color.White, + secondary = graySurface, + background = briarLightBlue, + surface = briarBlue, + onPrimary = Color.White, + onSecondary = lightGray, + onBackground = Color.White, + onSurface = Color.White, + error = Color.Red, +) \ No newline at end of file diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt new file mode 100644 index 0000000000..85454f9c57 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt @@ -0,0 +1,103 @@ +package org.briarproject.briar.desktop.paul.views + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.imageFromResource +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import org.briarproject.briar.desktop.paul.theme.briarBlack +import org.briarproject.briar.desktop.paul.theme.briarBlue + +@Composable +fun briarSidebar(UIMode: String, onModeChange: (String) -> Unit) { + Surface(modifier = Modifier.width(66.dp).fillMaxHeight(), color = briarBlue) { + Column(verticalArrangement = Arrangement.Top) { + IconButton( + modifier = Modifier.align(Alignment.CenterHorizontally) + .padding(top = 9.dp, bottom = 10.dp), onClick = {}) { + Image( + bitmap = imageFromResource("images/profile_images/p0.png"), + "my_profile_image", + modifier = Modifier.size(48.dp).align(Alignment.CenterHorizontally).clip( + CircleShape + ).border(2.dp, color = Color.White, CircleShape) + ) + } + briarSidebarButton( + UIMode = UIMode, + onModeChange = onModeChange, + "Contacts", + Icons.Filled.Contacts + ) + briarSidebarButton( + UIMode = UIMode, + onModeChange = onModeChange, + "Private Groups", + Icons.Filled.Group + ) + briarSidebarButton( + UIMode = UIMode, + onModeChange = onModeChange, + "Forums", + Icons.Filled.Forum + ) + briarSidebarButton( + UIMode = UIMode, + onModeChange = onModeChange, + "Blogs", + Icons.Filled.ChromeReaderMode + ) + } + Column(verticalArrangement = Arrangement.Bottom) { + briarSidebarButton( + UIMode = UIMode, + onModeChange = onModeChange, + "Transports", + Icons.Filled.WifiTethering + ) + briarSidebarButton( + UIMode = UIMode, + onModeChange = onModeChange, + "Settings", + Icons.Filled.Settings + ) + briarSidebarButton( + UIMode = UIMode, + onModeChange = onModeChange, + "Sign Out", + Icons.Filled.Logout + ) + } + } +} + +@Composable +fun briarSidebarButton( + UIMode: String, + onModeChange: (String) -> Unit, + thisMode: String, + icon: ImageVector +) { + val bg = if (UIMode == thisMode) briarBlack else briarBlue; + Column() { + IconButton( + modifier = Modifier.align(Alignment.CenterHorizontally).background(color = bg) + .padding(vertical = 9.dp, horizontal = 12.dp), + onClick = { onModeChange(thisMode) }) { + Icon(icon, thisMode, tint = Color.White, modifier = Modifier.size(30.dp)) + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt new file mode 100644 index 0000000000..a58fa8f036 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt @@ -0,0 +1,51 @@ +package org.briarproject.briar.desktop.paul.views + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import org.briarproject.briar.desktop.paul.data.ContactList +import org.briarproject.briar.desktop.paul.theme.briarBlack + +/* + * This is the root of the tree, all state is held here and passed down to stateless composables, which render the UI + * Desktop specific kotlin files are found in briarComposeDesktop (possibly briar-compose-desktop project in the future) + * Multiplatform, stateless, composable are found in briarCompose (possible briar-compose project in the future) + */ +@Composable +fun briarUIStateManager() { + //current selected mode, changed using the sidebar buttons + val (UIMode, onModeChange) = remember { mutableStateOf("Contacts") } + //current selected contact + val (UIContact, onContactSelect) = remember { mutableStateOf(ContactList.contacts[0]) } + //current selected private message + val (UIPrivateMsg, onPMSelect) = remember { mutableStateOf(0) } + //current selected forum + val (UIForum, onForumSelect) = remember { mutableStateOf(0) } + //current blog state + val (UIBlog, onBlogSelect) = remember { mutableStateOf(0) } + //current transport state + val (UITransports, onTransportSelect) = remember { mutableStateOf(0) } + //current settings state + val (UISettings, onSettingSelect) = remember { mutableStateOf(0) } + //current profile + var Profile: String; + //Other global state that we need to track should go here also + Row() { + briarSidebar(UIMode, onModeChange) + when (UIMode) { + "Contacts" -> privateMessageView(UIContact, onContactSelect) + else -> Box(modifier = Modifier.fillMaxSize().background(briarBlack)) { + Text("TBD", modifier = Modifier.align(Alignment.Center), color = Color.White) + } + } + } + +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt new file mode 100644 index 0000000000..fc8544d5e9 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt @@ -0,0 +1,319 @@ +package org.briarproject.briar.desktop.paul.views + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.imageFromResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.v1.DialogProperties +import org.briarproject.briar.desktop.paul.data.ContactList +import org.briarproject.briar.desktop.paul.model.Contact +import org.briarproject.briar.desktop.paul.model.Message +import org.briarproject.briar.desktop.paul.theme.* + + +val HEADER_SIZE = 66.dp; + +@Composable +fun privateMessageView(UIContact: Contact, onContactSelect: (Contact) -> Unit) { + //Local State for managing the Add Contact Popup + val (AddContactDialog, onCancelAdd) = remember { mutableStateOf(false) } + addContactDialog(AddContactDialog, onCancelAdd) + Column(modifier = Modifier.fillMaxHeight()) { + Row(modifier = Modifier.fillMaxWidth()) { + Divider(color = divider, modifier = Modifier.fillMaxHeight().width(1.dp)) + Column(modifier = Modifier.fillMaxHeight().background(color = briarBlack).width(275.dp)) { + Row( + modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + "Contacts", + fontSize = 24.sp, + color = Color.White, + modifier = Modifier.align(Alignment.CenterVertically) + ) + IconButton( + onClick = { onCancelAdd(true) }, + modifier = Modifier.align(Alignment.CenterVertically).background(color = briarDarkGray) + ) { + Icon(Icons.Filled.Add, "add contact", tint = Color.White, modifier = Modifier.size(24.dp)) + } + } + Divider(color = divider, thickness = 1.dp, modifier = Modifier.fillMaxWidth()) + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + for (c in ContactList.contacts) { + contactCard(c, UIContact, onSel = onContactSelect) + } + } + + } + Divider(color = divider, modifier = Modifier.fillMaxHeight().width(1.dp)) + Column(modifier = Modifier.weight(1f).fillMaxHeight().background(color = darkGray)) { + drawMessageRow(UIContact) + } + } + } +} + +@Composable +fun addContactDialog(isVisible: Boolean, onCancel: (Boolean) -> Unit) { + if (isVisible) { + AlertDialog( + onDismissRequest = { + onCancel(false) + }, + text = { + Column(modifier = Modifier.fillMaxWidth()) { + Row(Modifier.fillMaxWidth().padding(vertical = 16.dp)) { + Text( + text = "Add Contact at a Distance", + fontSize = 24.sp, + color = Color.White, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + "Contact's Link", + Modifier.width(128.dp).align(Alignment.CenterVertically), + color = lightGray + ) + TextField("", onValueChange = {}, modifier = Modifier.fillMaxWidth()) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + "Contact's Name", + Modifier.width(128.dp).align(Alignment.CenterVertically), + color = lightGray + ) + TextField("", onValueChange = {}, modifier = Modifier.fillMaxWidth()) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + "Your Link", + modifier = Modifier.width(128.dp).align(Alignment.CenterVertically), + color = lightGray + ) + TextField( + "briar://ksdjlfgakslhjgaklsjdhglkasjdlk3j12h4lk2j3tkj4", + onValueChange = {}, + modifier = Modifier.fillMaxWidth() + ); + } + } + }, + confirmButton = { + TextButton( + onClick = { + onCancel(false) + }, + modifier = Modifier.background(briarGreen) + ) { + Text("Add") + } + }, + dismissButton = { + TextButton( + onClick = { + onCancel(false) + }, modifier = Modifier.background(briarBlack) + ) { + Text("Cancel") + } + }, + + backgroundColor = briarBlue, + contentColor = Color.White, + modifier = Modifier.border(1.dp, color = divider), + properties = DialogProperties(resizable = false, undecorated = true, size = IntSize(600, 300)) + ) + } +} + +@Composable +fun contactCard(contact: Contact, selContact: Contact, onSel: (Contact) -> Unit) { + var bgColor = briarBlack + if (selContact.name == contact.name) { + bgColor = darkGray + } + Row( + modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).background(bgColor) + .clickable(onClick = { onSel(contact) }), horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(modifier = Modifier.align(Alignment.CenterVertically).padding(horizontal = 16.dp)) { + Image( + bitmap = imageFromResource("images/profile_images/" + contact.profile_pic), + "image", + modifier = Modifier.size(40.dp).align(Alignment.CenterVertically).clip( + CircleShape + ).border(2.dp, color = Color.White, CircleShape) + ) + Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp)) { + Text( + contact.name, + fontSize = 14.sp, + color = Color.White, + modifier = Modifier.align(Alignment.Start).padding(bottom = 2.dp) + ) + Text( + contact.last_heard, + fontSize = 10.sp, + color = Color.LightGray, + modifier = Modifier.align(Alignment.Start) + ) + } + } + androidx.compose.foundation.Canvas( + modifier = Modifier.padding(horizontal = 29.dp).size(22.dp).align(Alignment.CenterVertically), onDraw = { + val size = 16.dp.toPx() + drawCircle( + color = Color.White, + radius = size / 2f + ) + if (contact.online) { + drawCircle( + color = briarGreen, + radius = 14.dp.toPx() / 2f + ) + } else { + drawCircle( + color = briarBlack, + radius = 14.dp.toPx() / 2f + ) + } + }) + } + + Divider(color = divider, thickness = 1.dp, modifier = Modifier.fillMaxWidth()) +} + +@Composable +fun textBubble(m: Message) { + Column(Modifier.fillMaxWidth()) { + if (m.from == null) { + Column(Modifier.fillMaxWidth(fraction = 0.9f).align(Alignment.End)) { + Column(Modifier.background(briarBlueMsg).padding(8.dp).align(Alignment.End)) { + Text(m.message, fontSize = 14.sp, color = Color.White, modifier = Modifier.align(Alignment.Start)) + Row(modifier = Modifier.padding(top = 4.dp)) { + Text(m.time, Modifier.padding(end = 4.dp), fontSize = 10.sp, color = Color.LightGray) + if (m.delivered) { + Icon( + Icons.Filled.Check, + "sent", + tint = Color.LightGray, + modifier = Modifier.size(10.dp).align(Alignment.CenterVertically) + ) + } else { + Icon( + Icons.Filled.Send, + "sending", + tint = Color.LightGray, + modifier = Modifier.size(10.dp).align(Alignment.CenterVertically) + ) + } + } + } + } + } else { + Column(Modifier.fillMaxWidth(fraction = 0.9f).align(Alignment.Start)) { + Column(Modifier.background(briarGrayMsg).padding(8.dp).align(Alignment.Start)) { + Text(m.message, fontSize = 14.sp, color = Color.White, modifier = Modifier.align(Alignment.Start)) + Row(modifier = Modifier.padding(top = 4.dp)) { + Text(m.time, Modifier.padding(end = 4.dp), fontSize = 10.sp, color = Color.LightGray) + if (m.delivered) { + Icon( + Icons.Filled.Check, + "sent", + tint = Color.LightGray, + modifier = Modifier.size(10.dp).align(Alignment.CenterVertically) + ) + } else { + Icon( + Icons.Filled.Send, + "sending", + tint = Color.LightGray, + modifier = Modifier.size(10.dp).align(Alignment.CenterVertically) + ) + } + } + } + } + } + } +} + +@Composable +fun drawTextBubbles(msgList: List<Message>) { + LazyColumn( + Modifier.fillMaxWidth().padding(horizontal = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + reverseLayout = true, + contentPadding = PaddingValues(vertical = 8.dp) + ) { + items(msgList) { m -> + textBubble(m) + } + } +} + +@Composable +fun drawMessageRow(UIContact: Contact) { + Box(Modifier.fillMaxHeight()) { + Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) { + Row(modifier = Modifier.align(Alignment.Center)) { + Image( + bitmap = imageFromResource("images/profile_images/" + UIContact.profile_pic), + "sel_contact_prof", + modifier = Modifier.size(36.dp).align( + Alignment.CenterVertically + ).clip( + CircleShape + ).border(2.dp, color = Color.White, CircleShape) + ) + Text( + UIContact.name, + color = Color.White, + modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp), + fontSize = 24.sp + ) + } + IconButton(onClick = {}, modifier = Modifier.align(Alignment.CenterEnd).padding(end = 16.dp)) { + Icon(Icons.Filled.MoreVert, "contact info", tint = Color.White, modifier = Modifier.size(24.dp)) + } + Divider(color = divider, thickness = 1.dp, modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)) + } + Box(Modifier.padding(top = HEADER_SIZE + 1.dp, bottom = HEADER_SIZE)) { + drawTextBubbles(UIContact.privateMessages) + } + var text by remember { mutableStateOf(TextFieldValue("")) } + Box(Modifier.align(Alignment.BottomCenter).background(darkGray)) { + OutlinedTextField( + value = text, + trailingIcon = { Icon(Icons.Filled.Send, "send message", tint = briarGreen) }, + leadingIcon = { Icon(Icons.Filled.AddCircle, contentDescription = "add file") }, + modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp).fillMaxWidth(), + label = { Text(text = "Message") }, + textStyle = TextStyle(color = Color.White), + placeholder = { Text(text = "Your message to " + UIContact.name) }, + onValueChange = { + text = it + }, + ) + } + } +} \ No newline at end of file diff --git a/src/main/resources/images/profile_images/p0.png b/src/main/resources/images/profile_images/p0.png new file mode 100644 index 0000000000000000000000000000000000000000..8c67a3c592c8267f56a4c810cb2d561c88d05e18 GIT binary patch literal 45426 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/profile_images/p1.png b/src/main/resources/images/profile_images/p1.png new file mode 100644 index 0000000000000000000000000000000000000000..50155c8a1ebc3513e3b6e3cc2936881b594f92f4 GIT binary patch literal 2804 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/profile_images/p2.png b/src/main/resources/images/profile_images/p2.png new file mode 100644 index 0000000000000000000000000000000000000000..8ff9a6e10f151d8e9913a968a535cf7b3e542c0f GIT binary patch literal 45426 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/profile_images/p3.png b/src/main/resources/images/profile_images/p3.png new file mode 100644 index 0000000000000000000000000000000000000000..4b8509e1e21afe1863ad0d35395ef11cdd9113b4 GIT binary patch literal 45426 zcmeAS@N?(olHy`uVBq!ia0y~yV7LUr9Lx+13>@xx1q=+#3;{kNuK)l4@7Z>PVHAvp zz=#Y1kqoIx3=E76B|(0{|NoE3Ab@*XEB*R028OkYo-U3d6}K40*1iOl8*u4S#%KtP zhQMeDjE2By2#kinXb6mkz-S1JhQMeDjD`T75MU$>@aTp~5jKPorW6lN9a9*nW6WSO zWz<W!Qq5>A;RqS(1k7l{!x6lLDqsecV@4C+P)IVPu{0V>$Y}sqXdsCrE62e`QjLp= zLnpE@F0IJo$m(&hk(J|M<IssLj6)}~FtTzSY-HuQ*f_Kz3**v?B#x{c2OCK>E+!70 z$ild^B8wxd$H7Kcj)RRuC$caOoyfw-%5kufmE&UL(26XKODmE%vT_`3B-OZ>ICLTl z<I;*Oj;tOB8(BFHHV&Q0!Z>sy3nMGX!A4e&i;Y7ovM?^KNaD!Kaj=n8<6`2_i7bpu zE3!DUdK_$I<v7?lbRrAm(1|RJtQ-d$Svf8?4z0+-xU?dPBP++jMpBK7i9;u{FfOgg z;>hZ8u#uJHVB^q<EQ~`ZvM{o89BgFexY#(fA`9cviX@J#90waoH7+I&oyfwtv?7Zm ztH;4cR*r*>LnpE@4xPxt$jWiBk(J|O<IsvMj7uw$II?maY$VmVm^gGI3**v?ERL)m z2OC*A4mJ**$ig^uA`2rc$H7Kcj*E>$E3z;ytw`d?%5kufRO4de(1|RJODnQCvU(hB zWaT*6ICLTl<IssLjI1078(BFnHV&=G!nm{|i6bk=!A4Szi-|)gvM?^K$l}QAaj=n< z<6z^^i7bpmC$ccIavW@A<+#{5v?2@R(uyRGtQ-d$Ni{Ae4xPxtxU?dRBdf>3Mpll4 zjYB80Fb<u_!pO>Tu#uJHV&l+?ER0Jlk~p$*9Bd@jxR^L}A`9cviY$(-9tRs)ISw`s zoyfvCbRr8QE62e`R*s8}Lo2c{F0Dx7$jWiBkyPVi;?RjKj7#gN_-F`>hQMeDjE2By z2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1J zhQMeD45<)6o(LTRY+}N82zUe8iKAXZ4voRe9*w2J8Z*f58jU67&={=j(O4R+F@x-` z(O5zbjls$ujitdFGsx~5jV0vJ7_98kSQ@M`gY2%+SV9hs!O9+urNJ6A$nF}ACFIZ; ztnATP8muvc?5@#RLJp0=${vlS!5TBj?i!6H<j@$b?9o^ntTBV^t`Qnb#CUsD&1eXW zhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kin zXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeD zjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mk sz-S1JhQMeDjE2By2#kinXb23>5J*o`^DImB&H}m4)78&qol`;+071k=2mk;8 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/profile_images/p4.png b/src/main/resources/images/profile_images/p4.png new file mode 100644 index 0000000000000000000000000000000000000000..96ed7fbc717af1d66c71fd5b9376b6a32dfc6e5b GIT binary patch literal 45426 zcmeAS@N?(olHy`uVBq!ia0y~yV7LUr9Lx+13>@xx1q=+#3;{kNuK)l4@42ysVHAvp zz=#Zimyu8685kHDN`m}?|NkG6K>+u(R{Hg03=C@(JzX3_DsC}~t$hh9H{jBvjL{Gn z4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!3iMhX5mCKyx=x*#g2oVx+Q-G&gS4OEeE+ zDp)WYOH>F%nj0}1OEeE+Dp)WYOH>F%nj0}1OEeE+Dp)WYOH>F%nj0}1OEeE+Dp)WY zOH>F%nj0}1OEeE+Dp)WYOH>F%nj0}1OEeE+Dp)WYOH>F%nj0}1OEeE+Dp)WYOH>F% znj0}1OEeE+Dp)WYOH>F%nj0~wVu=XH5TS&KFeAbgB9ste5d#s%5TS$!n}{%l2qi>V zH0mW>;YdW_;nGT)I1v_&#uBa|B_i-}X(dgZ2#ZEz30II35qP+?k|s`sMWeBVD@ch5 zJX~5y6DPu=(OAM2q(lTBF0G`A6JgP4Ea3`LA_5PWR?@_YuxK=va0Mw5frm>gY2rj! zG#X2|f|Q8B!=;rpaUv`ljU`+`N<`q{(n^{*5f+Wc60RU6BJgl&B~6?Ni$-G!SCA4B zc(}BZCQgJ!qp^f5NQnqMTv|yJC&Hr9Si%*gL<Al#t)z()VbN$T;R;eB0uPr~(!`0d zXf&2^1t}4Mhf6DI;zU?98cVo>l!(B?rIj>sA}kt?u|$NoNef2`R1@JADk-7Bk)&y* zl6#3ThBS9kpqdD`P)P{|jwDShmE22&F{HVR0@Xygg-S{&a3pD3spMWFj3Lck6sRV` zEmTrMfg?%NN+tIaVGL>RqChnfZlRJA3LHtARw}ud2xCZd7X_+`a0```P~b??v{K2v zL>NPwyC_gigj=YjgaSvBrj<(WCBhie+(m(EBHThHB@{T4G_6!}FA>I&<}M0U6X6yr zDWSlTq-mv+dx<cHG<Q*;nh3X0NeKmxBuy)o+)IQpq`8X%)kL_3N=hhjBxzcy<X$3- zA<bPBs3yWKR8m5LBT3UrCHE3x3~BD7Ks6C=p^_2`97&p1D!G>kV@Pus1*(Z~3zd{m z;7HQ6QpvqU7(<%7C{Rs=Td1Uj0!Najl}hd<!Wh!rMS*G}+(IQK6gZMJtyFR^5yp__ zE(%l=;T9?>p}>)(X{C~Ti7<vVcTu342)B$X84ZEa5Eu=C(GVC7fzc2c4S~@R7!85Z z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=CkrDz#%)Sy4 z6x1<=2#bhOLLJ9Y#}s1RON1KgIEFf=5aC>6lu*Yp)G>t^_Y$FoI*y@^DMUDz7$wwk z40TK)#=S(Sp^jsyV+s+@B}NH#977#bh;c6wYN+EF>X<@=bBR$x9mi0|6k^;<gc|BN zhB~GY;ap;rP{%RUF@+fS5}}4Vj-iezL^ziiCDd^Ybxa|~y+o*?j$^1}3K7mFMhSHs zLmg9yaW4^SsN)#wm_meeiBUow$56)<V%$rF8tOQPI;IfeTw;_^$1&6~g&6k|p@uq+ zp^hm;IF}eD)Nu@TOd-a-M5v*TW2j>a5zZw>33VJp9aD&LFA-{};~46gLWFaPQ9>QZ zP{$Nv+)IQS>NtivrV!y=Vw6zFG1M`I821vPhB}U+jwwVqml!3~aSU}#A;!H#sG*Ky zsACEd&Lu_(bsR$-Q;2ad5o)O880wfpgmZ~eLLJ9Y#}s1RON1KgIEFf=5aHZWC8HrQ z8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O0~i7YDf+kXYi>3GIl<G_&t;uc GLK6UAC{OkP literal 0 HcmV?d00001 diff --git a/src/main/resources/images/profile_images/p5.png b/src/main/resources/images/profile_images/p5.png new file mode 100644 index 0000000000000000000000000000000000000000..72348d6d73b3a8cb15aa7f1269e5e980256c7388 GIT binary patch literal 45426 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 apiKzKtNKjL$bQHIGTqbF&t;ucLK6VyA8Qr> literal 0 HcmV?d00001 -- GitLab