diff --git a/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt b/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt index 96421379a9de135ad2c67e9f3af966a743e6c864..c96cd5a45913f6add92abffd7fa0cd8e11793007 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt @@ -1,6 +1,7 @@ package org.briarproject.briar.desktop import androidx.compose.desktop.Window +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf @@ -18,6 +19,7 @@ import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.messaging.MessagingManager import org.briarproject.briar.desktop.dialogs.Login import org.briarproject.briar.desktop.dialogs.Registration +import org.briarproject.briar.desktop.paul.theme.DarkColorPallet import org.briarproject.briar.desktop.paul.views.BriarUIStateManager import javax.annotation.concurrent.Immutable import javax.inject.Inject @@ -80,28 +82,30 @@ constructor( val title = "Briar Desktop" var screenState by remember { mutableStateOf<Screen>(Screen.Login) } Window(title = title) { - when (val screen = screenState) { - is Screen.Login -> - Login( - "Briar", - onResult = { - try { - accountManager.signIn(it) - signedIn() - screenState = Screen.Main - } catch (e: DecryptionException) { - // failure, try again + MaterialTheme(colors = DarkColorPallet) { + when (val screen = screenState) { + is Screen.Login -> + Login( + "Briar", + onResult = { + try { + accountManager.signIn(it) + signedIn() + screenState = Screen.Main + } catch (e: DecryptionException) { + // failure, try again + } } - } - ) + ) - is Screen.Main -> - CompositionLocalProvider( - CM provides conversationManager, - MM provides messagingManager - ) { - BriarUIStateManager(contacts) - } + is Screen.Main -> + CompositionLocalProvider( + CM provides conversationManager, + MM provides messagingManager + ) { + BriarUIStateManager(contacts) + } + } } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt index f1a532eb80b12d6e04a095bc35bbcea198fc8df5..2da6d8aa6d56fe03aceebf3e3840b645d1f6f2b2 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt @@ -1,6 +1,7 @@ package org.briarproject.briar.desktop.dialogs import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -20,8 +21,10 @@ import androidx.compose.runtime.setValue 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.res.svgResource import androidx.compose.ui.unit.dp +import org.briarproject.briar.desktop.paul.theme.briarBlack // TODO: Error handling @Composable @@ -29,8 +32,9 @@ fun Login( title: String, onResult: (result: String) -> Unit ) = + // All the changes in this file are be temporary -Paul, just changing colors so I can see the button and text field Column( - modifier = Modifier.padding(16.dp).fillMaxSize(), + modifier = Modifier.padding(16.dp).fillMaxSize().background(briarBlack), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -60,6 +64,6 @@ private fun TheTextField(onResult: (result: String) -> Unit) { onResult.invoke(password) } ) { - Text("Login") + Text("Login", color = Color.Black) } } 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 index 64dbb66c523c61c4ab84570259a18a2437c028ab..e4e3dccf4a511a26126b8f72c935ab61c98ae50c 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt @@ -33,43 +33,42 @@ 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) { +fun BriarSidebar(uiMode: String, setMode: (String) -> Unit) { + Surface(modifier = Modifier.width(56.dp).fillMaxHeight(), color = briarBlue) { Column(verticalArrangement = Arrangement.Top) { IconButton( - modifier = Modifier.align(Alignment.CenterHorizontally) - .padding(top = 9.dp, bottom = 10.dp), + modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 5.dp, bottom = 4.dp), onClick = {} ) { Image( bitmap = imageFromResource("images/profile_images/p0.png"), "my_profile_image", - modifier = Modifier.size(48.dp).align(Alignment.CenterHorizontally).clip( + modifier = Modifier.size(44.dp).align(Alignment.CenterHorizontally).clip( CircleShape ).border(2.dp, color = Color.White, CircleShape) ) } BriarSidebarButton( uiMode = uiMode, - onModeChange = onModeChange, + setMode = setMode, "Contacts", Icons.Filled.Contacts ) BriarSidebarButton( uiMode = uiMode, - onModeChange = onModeChange, + setMode = setMode, "Private Groups", Icons.Filled.Group ) BriarSidebarButton( uiMode = uiMode, - onModeChange = onModeChange, + setMode = setMode, "Forums", Icons.Filled.Forum ) BriarSidebarButton( uiMode = uiMode, - onModeChange = onModeChange, + setMode = setMode, "Blogs", Icons.Filled.ChromeReaderMode ) @@ -77,19 +76,19 @@ fun BriarSidebar(uiMode: String, onModeChange: (String) -> Unit) { Column(verticalArrangement = Arrangement.Bottom) { BriarSidebarButton( uiMode = uiMode, - onModeChange = onModeChange, + setMode = setMode, "Transports", Icons.Filled.WifiTethering ) BriarSidebarButton( uiMode = uiMode, - onModeChange = onModeChange, + setMode = setMode, "Settings", Icons.Filled.Settings ) BriarSidebarButton( uiMode = uiMode, - onModeChange = onModeChange, + setMode = setMode, "Sign Out", Icons.Filled.Logout ) @@ -98,18 +97,13 @@ fun BriarSidebar(uiMode: String, onModeChange: (String) -> Unit) { } @Composable -fun BriarSidebarButton( - uiMode: String, - onModeChange: (String) -> Unit, - thisMode: String, - icon: ImageVector -) { +fun BriarSidebarButton(uiMode: String, setMode: (String) -> Unit, thisMode: String, icon: ImageVector) { val bg = if (uiMode == thisMode) briarBlack else briarBlue - Column() { + Column { IconButton( modifier = Modifier.align(Alignment.CenterHorizontally).background(color = bg) - .padding(vertical = 9.dp, horizontal = 12.dp), - onClick = { onModeChange(thisMode) } + .padding(vertical = 4.dp, horizontal = 12.dp), + onClick = { setMode(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 index e0b9c0c40275bb3f7d44c7a69acf43b5982f42d2..188ca429701b4c14b78dc4480925d68c522bc04e 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt @@ -6,7 +6,6 @@ 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.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -25,28 +24,26 @@ fun BriarUIStateManager( contacts: List<Contact> ) { // current selected mode, changed using the sidebar buttons - val (uiMode, onModeChange) = remember { mutableStateOf("Contacts") } + val (uiMode, setMode) = remember { mutableStateOf("Contacts") } + // TODO Figure out how to handle accounts with 0 contacts // current selected contact - val uiContact: MutableState<Contact> = remember { mutableStateOf(contacts[0]) } - // current selected private message - val (uiPrivateMsg, onPMSelect) = remember { mutableStateOf(0) } + val (contact, setContact) = remember { mutableStateOf(contacts[0]) } // current selected forum - val (uiForum, onForumSelect) = remember { mutableStateOf(0) } + val (forum, setForum) = remember { mutableStateOf(0) } // current blog state - val (uiBlog, onBlogSelect) = remember { mutableStateOf(0) } + val (blog, setBlog) = remember { mutableStateOf(0) } // current transport state - val (uiTransports, onTransportSelect) = remember { mutableStateOf(0) } + val (transport, setTransport) = remember { mutableStateOf(0) } // current settings state - val (uiSettings, onSettingSelect) = remember { mutableStateOf(0) } - // current profile - var Profile: String + val (setting, setSetting) = remember { mutableStateOf(0) } // Other global state that we need to track should go here also Row() { - BriarSidebar(uiMode, onModeChange) + BriarSidebar(uiMode, setMode) when (uiMode) { "Contacts" -> PrivateMessageView( + contact, contacts, - uiContact + setContact ) 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 index 65971e55ea823c382d19d4906ffb1f4f721b64c8..d8483431ddf0c2d9193db55313726d2bcaf40f2e 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt @@ -1,40 +1,55 @@ package org.briarproject.briar.desktop.paul.views +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material.AlertDialog import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.IconButton -import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.TextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.AddCircle -import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowRight +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.PersonAdd +import androidx.compose.material.icons.filled.Schedule +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState @@ -46,6 +61,8 @@ 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.SolidColor +import androidx.compose.ui.graphics.drawscope.withTransform import androidx.compose.ui.graphics.imageFromResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue @@ -74,47 +91,41 @@ import org.briarproject.briar.desktop.paul.theme.divider import org.briarproject.briar.desktop.paul.theme.lightGray import java.util.Collections -val HEADER_SIZE = 66.dp +val HEADER_SIZE = 56.dp + +// Right drawer state +enum class ContactInfoDrawerState { + MakeIntro, + ConnectBT, + ConnectRD +} @Composable fun PrivateMessageView( + currContact: Contact, contacts: List<Contact>, - uiContact: MutableState<Contact> + onContactSelect: (Contact) -> Unit ) { - // Local State for managing the Add Contact Popup - val (AddContactDialog, onCancelAdd) = remember { mutableStateOf(false) } - AddContactDialog(AddContactDialog, onCancelAdd) + val (addContactDialog, onContactAdd) = remember { mutableStateOf(false) } + val (dropdownExpanded, setExpanded) = remember { mutableStateOf(false) } + val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) } + val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) } + AddContactDialog(addContactDialog, onContactAdd) + Divider(color = divider, modifier = Modifier.fillMaxHeight().width(1.dp)) + ContactList(currContact, contacts, onContactSelect, onContactAdd) 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 contacts) { - ContactCard(uiContact, c) - } - } - } Divider(color = divider, modifier = Modifier.fillMaxHeight().width(1.dp)) Column(modifier = Modifier.weight(1f).fillMaxHeight().background(color = darkGray)) { - DrawMessageRow(uiContact.value) + DrawMessageRow( + currContact, + contacts, + dropdownExpanded, + setExpanded, + infoDrawer, + setInfoDrawer, + contactDrawerState + ) } } } @@ -198,30 +209,87 @@ fun AddContactDialog(isVisible: Boolean, onCancel: (Boolean) -> Unit) { } @Composable -fun ContactCard(selContact: MutableState<Contact>, contact: Contact) { +fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onContactAdd: (Boolean) -> Unit) { + BasicTextField( + value = searchValue, + onValueChange = onValueChange, + singleLine = true, + modifier = Modifier.padding(horizontal = 8.dp), + textStyle = TextStyle(color = Color.White, fontSize = 16.sp), + decorationBox = { innerTextField -> + Row( + Modifier + .background(darkGray, CircleShape) + .border(1.dp, divider, CircleShape) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Icon( + Icons.Filled.Search, + "search contacts", + tint = Color.White, + modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 16.dp) + ) + Box(Modifier.width(132.dp).padding(vertical = 8.dp, horizontal = 2.dp)) { + if (searchValue.isEmpty()) { + Text("Contacts", color = Color.Gray) + } + innerTextField() + } + IconButton( + onClick = { onContactAdd(true) }, + modifier = Modifier.padding(end = 4.dp).size(32.dp).background( + briarBlueMsg, CircleShape + ) + ) { + Icon(Icons.Filled.PersonAdd, "add contact", tint = Color.White, modifier = Modifier.size(20.dp)) + } + } + }, + cursorBrush = SolidColor(Color.White), + ) +} + +@Composable +fun ContactCard(contact: Contact, selContact: Contact, onSel: (Contact) -> Unit, drawDetails: Boolean) { var bgColor = briarBlack - if (selContact.value.id == contact.id) { + if (selContact.id == contact.id && drawDetails) { bgColor = darkGray } Row( modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).background(bgColor) - .clickable( - onClick = { - selContact.value = contact - } - ), + .clickable(onClick = { onSel(contact) }), horizontalArrangement = Arrangement.SpaceBetween ) { Row(modifier = Modifier.align(Alignment.CenterVertically).padding(horizontal = 16.dp)) { + // TODO Pull profile pictures Image( - // TODO: use correct image - // bitmap = imageFromResource("images/profile_images/" + contact.profile_pic), - bitmap = imageFromResource("images/profile_images/p1.png"), + bitmap = imageFromResource("images/profile_images/p0.png"), "image", - modifier = Modifier.size(40.dp).align(Alignment.CenterVertically).clip( + modifier = Modifier.size(36.dp).align(Alignment.CenterVertically).clip( CircleShape ).border(2.dp, color = Color.White, CircleShape) ) + // Draw notification badges + if (drawDetails) { + androidx.compose.foundation.Canvas( + modifier = Modifier.align(Alignment.CenterVertically), + onDraw = { + val size = 10.dp.toPx() + withTransform({ translate(left = -6f, top = -12f) }) { + drawCircle( + color = Color.White, + radius = (size + 2.dp.toPx()) / 2f, + ) + drawCircle( + color = briarBlueMsg, + radius = size / 2f, + ) + } + } + ) + } Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp)) { Text( contact.author.name, @@ -229,8 +297,9 @@ fun ContactCard(selContact: MutableState<Contact>, contact: Contact) { color = Color.White, modifier = Modifier.align(Alignment.Start).padding(bottom = 2.dp) ) + // TODO add proper last message time Text( - "1 min", + "10 min ago", fontSize = 10.sp, color = Color.LightGray, modifier = Modifier.align(Alignment.Start) @@ -238,15 +307,15 @@ fun ContactCard(selContact: MutableState<Contact>, contact: Contact) { } } androidx.compose.foundation.Canvas( - modifier = Modifier.padding(horizontal = 29.dp).size(22.dp).align(Alignment.CenterVertically), + modifier = Modifier.padding(start = 32.dp, end = 18.dp).size(22.dp).align(Alignment.CenterVertically), onDraw = { val size = 16.dp.toPx() drawCircle( color = Color.White, radius = size / 2f ) - val online = true - if (online) { + // TODO check if contact online + if (true) { drawCircle( color = briarGreen, radius = 14.dp.toPx() / 2f @@ -260,32 +329,80 @@ fun ContactCard(selContact: MutableState<Contact>, contact: Contact) { } ) } - Divider(color = divider, thickness = 1.dp, modifier = Modifier.fillMaxWidth()) } +@Composable +fun ContactList( + currContact: Contact, + contacts: List<Contact>, + onContactSelect: (Contact) -> Unit, + onContactAdd: (Boolean) -> Unit +) { + var searchValue by remember { mutableStateOf("") } + var filteredContacts = ArrayList<Contact>() + filteredContacts = if (searchValue.isEmpty()) { + ArrayList(contacts) + } else { + val resultList = ArrayList<Contact>() + for (c in contacts) { + if (c.author.name.lowercase().contains(searchValue.lowercase()) + ) { + resultList.add(c) + } + } + resultList + } + Scaffold( + modifier = Modifier.fillMaxHeight().width(246.dp), + backgroundColor = briarBlack, + topBar = { + Column( + modifier = Modifier.fillMaxWidth().background(briarBlack), + ) { + Row(Modifier.height(HEADER_SIZE), verticalAlignment = Alignment.CenterVertically) { + SearchTextField(searchValue, onValueChange = { searchValue = it }, onContactAdd) + } + Divider(color = divider, thickness = 1.dp, modifier = Modifier.fillMaxWidth()) + } + }, + content = { + Column(Modifier.verticalScroll(rememberScrollState())) { + for (c in filteredContacts) { + ContactCard(c, currContact, onContactSelect, true) + } + } + }, + ) +} + @Composable fun TextBubble(m: SimpleMessage) { Column(Modifier.fillMaxWidth()) { if (m.local) { - Column(Modifier.fillMaxWidth(fraction = 0.9f).align(Alignment.End)) { - Column(Modifier.background(briarBlueMsg).padding(8.dp).align(Alignment.End)) { + Column(Modifier.fillMaxWidth(fraction = 0.8f).align(Alignment.End)) { + Column( + Modifier.background( + briarBlueMsg, + RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp) + ).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, + Icons.Filled.DoneAll, "sent", tint = Color.LightGray, - modifier = Modifier.size(10.dp).align(Alignment.CenterVertically) + modifier = Modifier.size(12.dp).align(Alignment.CenterVertically) ) } else { Icon( - Icons.Filled.Send, + Icons.Filled.Schedule, "sending", tint = Color.LightGray, - modifier = Modifier.size(10.dp).align(Alignment.CenterVertically) + modifier = Modifier.size(12.dp).align(Alignment.CenterVertically) ) } } @@ -293,25 +410,28 @@ fun TextBubble(m: SimpleMessage) { } } else { Column(Modifier.fillMaxWidth(fraction = 0.9f).align(Alignment.Start)) { - Column(Modifier.background(briarGrayMsg).padding(8.dp).align(Alignment.Start)) { + Column( + Modifier.background( + briarGrayMsg, + RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp) + ).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, + Icons.Filled.DoneAll, "sent", tint = Color.LightGray, - modifier = Modifier.size(10.dp).align(Alignment.CenterVertically) + modifier = Modifier.size(12.dp).align(Alignment.CenterVertically) ) } else { Icon( - Icons.Filled.Send, + Icons.Filled.Schedule, "sending", tint = Color.LightGray, - modifier = Modifier.size(10.dp).align( - Alignment.CenterVertically - ) + modifier = Modifier.size(12.dp).align(Alignment.CenterVertically) ) } } @@ -328,10 +448,9 @@ fun DrawTextBubbles(chat: UiState<Chat>) { is UiState.Error -> Loader() is UiState.Success -> LazyColumn( - Modifier.fillMaxWidth().padding(horizontal = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), reverseLayout = true, - contentPadding = PaddingValues(vertical = 8.dp) + contentPadding = PaddingValues(top = 8.dp, start = 8.dp, end = 8.dp) ) { items(chat.data.messages) { m -> TextBubble(m) @@ -351,55 +470,340 @@ fun Loader() { } @Composable -fun DrawMessageRow(uiContact: Contact) { - Box(Modifier.fillMaxHeight()) { - Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) { - Row(modifier = Modifier.align(Alignment.Center)) { - Image( - // TODO: use correct image - // bitmap = imageFromResource("images/profile_images/" + UIContact.profile_pic), - bitmap = imageFromResource("images/profile_images/p2.png"), - "sel_contact_prof", - modifier = Modifier.size(36.dp).align( - Alignment.CenterVertically - ).clip( - CircleShape - ).border(2.dp, color = Color.White, CircleShape) +fun ContactDropDown( + expanded: Boolean, + isExpanded: (Boolean) -> Unit, + isInfoDrawer: (Boolean) -> Unit +) { + var connectionMode by remember { mutableStateOf(false) } + var contactMode by remember { mutableStateOf(false) } + DropdownMenu( + expanded = expanded, + onDismissRequest = { isExpanded(false) }, + modifier = Modifier.background(briarBlack) + ) { + DropdownMenuItem(onClick = { isInfoDrawer(true); isExpanded(false) }) { + Text("Make Introduction", fontSize = 14.sp) + } + DropdownMenuItem(onClick = {}) { + Text("Disappearing Messages", fontSize = 14.sp) + } + DropdownMenuItem(onClick = {}) { + Text("Delete all messages", fontSize = 14.sp) + } + DropdownMenuItem(onClick = { connectionMode = true; isExpanded(false) }) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text("Connections", fontSize = 14.sp, modifier = Modifier.align(Alignment.CenterVertically)) + Icon(Icons.Filled.ArrowRight, "connections", modifier = Modifier.align(Alignment.CenterVertically)) + } + } + DropdownMenuItem(onClick = { contactMode = true; isExpanded(false) }) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text("Contact", fontSize = 14.sp, modifier = Modifier.align(Alignment.CenterVertically)) + Icon(Icons.Filled.ArrowRight, "connections", modifier = Modifier.align(Alignment.CenterVertically)) + } + } + } + if (connectionMode) { + DropdownMenu( + expanded = connectionMode, + onDismissRequest = { connectionMode = false }, + modifier = Modifier.background(briarBlack) + ) { + DropdownMenuItem(onClick = { false }) { + Text("Connections", color = lightGray, fontSize = 12.sp) + } + DropdownMenuItem(onClick = { false }) { + Text("Connect via Bluetooth", fontSize = 14.sp) + } + DropdownMenuItem(onClick = { false }) { + Text("Connect via Removable Device", fontSize = 14.sp) + } + } + } + if (contactMode) { + DropdownMenu( + expanded = contactMode, + onDismissRequest = { contactMode = false }, + modifier = Modifier.background(briarBlack) + ) { + DropdownMenuItem(onClick = { false }) { + Text("Contact", color = lightGray, fontSize = 12.sp) + } + DropdownMenuItem(onClick = { false }) { + Text("Change contact name", fontSize = 14.sp) + } + DropdownMenuItem(onClick = { false }) { + Text("Delete contact", fontSize = 14.sp) + } + } + } +} + +@Composable +fun MsgColumnHeader( + uiContact: Contact, + expanded: Boolean, + isExpanded: (Boolean) -> Unit, + isInfoDrawer: (Boolean) -> Unit +) { + Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) { + Row(modifier = Modifier.align(Alignment.Center)) { + Image( + // TODO Fix profile picture resources + bitmap = imageFromResource("images/profile_images/p0.png"), + "sel_contact_prof", + modifier = Modifier.size(36.dp).align( + Alignment.CenterVertically + ).clip( + CircleShape + ).border(2.dp, color = Color.White, CircleShape) + ) + androidx.compose.foundation.Canvas( + modifier = Modifier.align(Alignment.CenterVertically), + onDraw = { + val size = 10.dp.toPx() + // TODO hook up online indicator logic + val onlineColor = if (true) briarGreen else briarBlack + withTransform({ translate(left = -6f, top = 12f) }) { + drawCircle( + color = Color.White, + radius = (size + 2.dp.toPx()) / 2f, + ) + drawCircle( + color = onlineColor, + radius = size / 2f, + ) + } + } + ) + Text( + uiContact.author.name, + color = Color.White, + modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp), + fontSize = 20.sp + ) + } + IconButton( + onClick = { isExpanded(!expanded) }, + modifier = Modifier.align(Alignment.CenterEnd).padding(end = 16.dp) + ) { + Icon(Icons.Filled.MoreVert, "contact info", tint = Color.White, modifier = Modifier.size(24.dp)) + ContactDropDown(expanded, isExpanded, isInfoDrawer) + } + Divider(color = divider, thickness = 1.dp, modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)) + } +} + +@Composable +fun MsgInput(currContact: Contact) { + var text by remember { mutableStateOf("") } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(8.dp) + ) { + BasicTextField( + value = text, + onValueChange = { text = it }, + maxLines = 10, + textStyle = TextStyle(color = Color.White, fontSize = 16.sp, lineHeight = 16.sp), + decorationBox = { innerTextField -> + Box( + Modifier + .background(darkGray, RoundedCornerShape(size = 20.dp)) + .border(1.dp, divider, RoundedCornerShape(size = 20.dp)) + .fillMaxWidth(), + contentAlignment = Alignment.TopCenter, + ) { + IconButton( + onClick = {}, + Modifier.padding(4.dp).size(32.dp).align(Alignment.TopStart) + .background(briarBlueMsg, CircleShape), + ) { + Icon( + Icons.Filled.Add, + "add attachment", + tint = Color.White, + modifier = Modifier.size(24.dp), + ) + } + Box( + Modifier.padding(vertical = 8.dp, horizontal = 48.dp).align(Alignment.Center).fillMaxWidth() + ) { + if (text.isEmpty()) { + Text("Message", color = Color.Gray) + } + innerTextField() + } + IconButton( + onClick = { }, + modifier = Modifier.padding(4.dp).size(32.dp).align(Alignment.TopEnd), + ) { + Icon(Icons.Filled.Send, "send message", tint = briarGreen, modifier = Modifier.size(24.dp)) + } + } + }, + cursorBrush = SolidColor(Color.White), + ) + } +} + +@Composable +fun ContactDrawerMakeIntro(currContact: Contact, contacts: List<Contact>, isInfoDrawer: (Boolean) -> Unit) { + var introNextPg by remember { mutableStateOf(false) } + val (introContact, onCancelSel) = remember { mutableStateOf(currContact) } + if (!introNextPg) { + Column() { + Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { + IconButton( + onClick = { isInfoDrawer(false) }, + Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically) + ) { + Icon(Icons.Filled.Close, "close make intro screen", tint = Color.White) + } + Text( + text = "Introduce " + currContact.author.name + " to:", + color = Color.White, + fontSize = 16.sp, + modifier = Modifier.align(Alignment.CenterVertically) ) + } + Divider(color = divider, modifier = Modifier.fillMaxWidth().height(1.dp)) + Column(Modifier.verticalScroll(rememberScrollState())) { + for (c in contacts) { + if (c.id != currContact.id) { + ContactCard(c, currContact, { onCancelSel(c); introNextPg = true }, false) + } + } + } + } + } else { + Column() { + Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { + IconButton( + onClick = { introNextPg = false }, + Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically) + ) { + Icon(Icons.Filled.ArrowBack, "go back to make intro contact screen", tint = Color.White) + } Text( - uiContact.author.name, + text = "Introduce Contacts", color = Color.White, - modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp), - fontSize = 24.sp + fontSize = 16.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + // Divider(color = divider, modifier = Modifier.fillMaxWidth().height(1.dp) ) + Row(Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.SpaceAround) { + Column(Modifier.align(Alignment.CenterVertically)) { + Image( + // TODO Proper profile pic + bitmap = imageFromResource("images/profile_images/p0.png"), + "image", + modifier = Modifier.size(40.dp).align(Alignment.CenterHorizontally).clip( + CircleShape + ).border(2.dp, color = Color.White, CircleShape) + ) + Text( + currContact.author.name, + color = Color.White, + fontSize = 16.sp, + modifier = Modifier.padding(top = 4.dp) + ) + } + Icon(Icons.Filled.SwapHoriz, "swap", tint = Color.White, modifier = Modifier.size(48.dp)) + Column(Modifier.align(Alignment.CenterVertically)) { + // TODO Profile pic again + Image( + bitmap = imageFromResource("images/profile_images/p0.png"), + "image", + modifier = Modifier.size(40.dp).align(Alignment.CenterHorizontally).clip( + CircleShape + ).border(2.dp, color = Color.White, CircleShape) + ) + Text( + introContact.author.name, + color = Color.White, + fontSize = 16.sp, + modifier = Modifier.padding(top = 4.dp) + ) + } + } + var introText by remember { mutableStateOf(TextFieldValue("")) } + Row(Modifier.padding(8.dp)) { + TextField( + introText, + { introText = it }, + placeholder = { Text(text = "Add a message (optional)") }, + textStyle = TextStyle(color = Color.White) ) } - 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)) + Row(Modifier.padding(8.dp)) { + TextButton( + onClick = { isInfoDrawer(false); introNextPg = false; }, + Modifier.fillMaxWidth().background(briarDarkGray) + ) { + Text("MAKE INTRODUCTION") + } } - Divider( - color = divider, - thickness = 1.dp, - modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter) - ) } - Box(Modifier.padding(top = HEADER_SIZE + 1.dp, bottom = HEADER_SIZE)) { - val chat = ChatState(uiContact.id) - DrawTextBubbles(chat.value) + } +} + +@Composable +fun ContactInfoDrawer( + currContact: Contact, + contacts: List<Contact>, + isInfoDrawer: (Boolean) -> Unit, + drawerState: ContactInfoDrawerState +) { + Row() { + when (drawerState) { + ContactInfoDrawerState.MakeIntro -> ContactDrawerMakeIntro(currContact, contacts, isInfoDrawer) } - 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.author.name) }, - onValueChange = { - text = it - }, - ) + } +} + +@Composable +fun DrawMessageRow( + currContact: Contact, + contacts: List<Contact>, + expanded: Boolean, + setExpanded: (Boolean) -> Unit, + infoDrawer: Boolean, + setInfoDrawer: (Boolean) -> Unit, + drawerState: ContactInfoDrawerState +) { + BoxWithConstraints(Modifier.fillMaxSize()) { + val animatedInfoDrawerOffsetX by animateDpAsState( + if (infoDrawer) { + -275.dp + } else { + 0.dp + } + ) + Scaffold( + topBar = { MsgColumnHeader(currContact, expanded, setExpanded, setInfoDrawer) }, + content = { padding -> + Box(modifier = Modifier.padding(padding)) { + val chat = ChatState(currContact.id) + DrawTextBubbles(chat.value) + } + }, + bottomBar = { MsgInput(currContact) }, + backgroundColor = darkGray, + modifier = Modifier.offset() + ) + if (infoDrawer) { + // TODO Find non-hacky way of setting scrim + // This dims the entire app while the drawer is open by making a very very large slightly see-through black box + Box(Modifier.requiredSize(maxWidth, maxHeight).background(Color(0, 0, 0, 100))) + Column( + modifier = Modifier.fillMaxHeight().width(275.dp).offset(maxWidth + animatedInfoDrawerOffsetX) + .background(briarBlack, RoundedCornerShape(topStart = 10.dp, bottomStart = 10.dp)) + ) { + ContactInfoDrawer(currContact, contacts, setInfoDrawer, drawerState) + } } } }