diff --git a/src/main/kotlin/org/briarproject/briar/desktop/Main.kt b/src/main/kotlin/org/briarproject/briar/desktop/Main.kt index 1c401a2cfc3fbcdd11e814673272a4bd9b6ccacd..d2347fa819a7bf5babfd915804941839b6ee3aec 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/Main.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/Main.kt @@ -8,6 +8,7 @@ import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import org.briarproject.bramble.BrambleCoreEagerSingletons import org.briarproject.briar.BriarCoreEagerSingletons +import org.briarproject.briar.desktop.utils.FileUtils import java.io.File.separator import java.io.IOException import java.lang.System.getProperty diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..ec879bd3c4788bdd343a29c5e783fe4f1df549e0 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt @@ -0,0 +1,142 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.briarproject.bramble.api.FormatException +import org.briarproject.bramble.api.contact.ContactManager +import org.briarproject.bramble.api.db.ContactExistsException +import org.briarproject.bramble.api.db.PendingContactExistsException +import org.briarproject.bramble.api.identity.AuthorConstants +import org.briarproject.bramble.util.StringUtils +import org.briarproject.briar.desktop.CTM +import java.security.GeneralSecurityException + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun AddContactDialog(isVisible: Boolean, setDialogVisibility: (Boolean) -> Unit) { + if (!isVisible) { + return + } + var contactAlias by remember { mutableStateOf("") } + var contactLink by remember { mutableStateOf("") } + val contactManager = CTM.current + val ownLink = CTM.current.handshakeLink + AlertDialog( + onDismissRequest = { setDialogVisibility(false) }, + text = { + Column(modifier = Modifier.fillMaxWidth()) { + Row(Modifier.fillMaxWidth().padding(vertical = 16.dp)) { + Text( + text = "Add Contact at a Distance", + fontSize = 24.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + "Contact's Link", + Modifier.width(128.dp).align(Alignment.CenterVertically), + ) + TextField(contactLink, onValueChange = { contactLink = it }, modifier = Modifier.fillMaxWidth()) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + "Contact's Name", + Modifier.width(128.dp).align(Alignment.CenterVertically), + ) + TextField(contactAlias, onValueChange = { contactAlias = it }, modifier = Modifier.fillMaxWidth()) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + "Your Link", + modifier = Modifier.width(128.dp).align(Alignment.CenterVertically), + ) + TextField( + ownLink, + onValueChange = {}, + modifier = Modifier.fillMaxWidth() + ) + } + } + }, + confirmButton = { + Button( + onClick = { + if (ownLink.equals(contactLink)) { + println("Please enter contact's link, not your own") + setDialogVisibility(false) + return@Button + } + addPendingContact(contactManager, contactAlias, contactLink) + setDialogVisibility(false) + }, + ) { + Text("Add") + } + }, + dismissButton = { + TextButton( + onClick = { setDialogVisibility(false) } + ) { + Text("Cancel", color = MaterialTheme.colors.onSurface) + } + }, + modifier = Modifier.size(600.dp, 300.dp), + ) +} + +private fun addPendingContact(contactManager: ContactManager, alias: String, link: String) { + if (aliasIsInvalid(alias)) { + println("Alias is invalid") + return + } + try { + contactManager.addPendingContact(link, alias) + } catch (e: FormatException) { + println("Link is invalid") + println(e.stackTrace) + } catch (e: GeneralSecurityException) { + println("Public key is invalid") + println(e.stackTrace) + } + /* + TODO: Warn user that the following two errors might be an attack + + Use `e.pendingContact.id.bytes` and `e.pendingContact.alias` to implement the following logic: + https://code.briarproject.org/briar/briar-gtk/-/merge_requests/97 + + */ + catch (e: ContactExistsException) { + println("Contact already exists") + println(e.stackTrace) + } catch (e: PendingContactExistsException) { + println("Pending Contact already exists") + println(e.stackTrace) + } +} + +private fun aliasIsInvalid(alias: String): Boolean { + val aliasUtf8 = StringUtils.toUtf8(alias) + return aliasUtf8.isEmpty() || aliasUtf8.size > AuthorConstants.MAX_AUTHOR_NAME_LENGTH +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..9af1d64f1fb90cd0ae981ce79fe003d6a6691f57 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt @@ -0,0 +1,98 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.briar.desktop.CVM +import org.briarproject.briar.desktop.contact.ProfileCircle +import org.briarproject.briar.desktop.theme.outline +import org.briarproject.briar.desktop.theme.selectedCard +import org.briarproject.briar.desktop.theme.surfaceVariant +import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE +import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp + +@Composable +fun ContactCard( + contact: Contact, + selContact: Contact, + onSel: (Contact) -> Unit, + selected: Boolean, + drawNotifications: Boolean +) { + val bgColor = if (selected) MaterialTheme.colors.selectedCard else MaterialTheme.colors.surfaceVariant + val outlineColor = MaterialTheme.colors.outline + val briarPrimary = MaterialTheme.colors.primary + val briarSecondary = MaterialTheme.colors.secondary + val briarSurfaceVar = MaterialTheme.colors.surfaceVariant + + Card( + modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).clickable(onClick = { onSel(contact) }), + shape = RoundedCornerShape(0.dp), + backgroundColor = bgColor, + contentColor = MaterialTheme.colors.onSurface + ) { + Row(horizontalArrangement = Arrangement.SpaceBetween) { + Row(modifier = Modifier.align(Alignment.CenterVertically).padding(horizontal = 16.dp)) { + // TODO Pull profile pictures + ProfileCircle(36.dp, contact.author.id.bytes) + // Draw notification badges + if (drawNotifications) { + Canvas( + modifier = Modifier.align(Alignment.CenterVertically), + onDraw = { + val size = 10.dp.toPx() + withTransform({ translate(left = -6f, top = -12f) }) { + drawCircle(color = outlineColor, radius = (size + 2.dp.toPx()) / 2f) + drawCircle(color = briarPrimary, radius = size / 2f) + } + } + ) + } + Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp)) { + Text( + contact.author.name, + fontSize = 14.sp, + modifier = Modifier.align(Alignment.Start).padding(bottom = 2.dp) + ) + val latestMsgTime = CVM.current.getGroupCount(contact.id).latestMsgTime + Text( + getFormattedTimestamp(latestMsgTime), + fontSize = 10.sp, + modifier = Modifier.align(Alignment.Start) + ) + } + } + Canvas( + modifier = Modifier.padding(start = 32.dp, end = 18.dp).size(22.dp).align(Alignment.CenterVertically), + onDraw = { + val size = 16.dp.toPx() + drawCircle(color = outlineColor, radius = size / 2f) + // TODO check if contact online + if (true) { + drawCircle(color = briarSecondary, radius = 14.dp.toPx() / 2f) + } else { + drawCircle(color = briarSurfaceVar, radius = 14.dp.toPx() / 2f) + } + } + ) + } + } + HorizontalDivider() +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt new file mode 100644 index 0000000000000000000000000000000000000000..fcb166e9151a4359da2dc79e3bb270d4af5f3bd8 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt @@ -0,0 +1,109 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Surface +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.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.briar.desktop.contact.ProfileCircle +import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE + +@Composable +fun ContactDrawerMakeIntro(contact: Contact, contacts: List<Contact>, setInfoDrawer: (Boolean) -> Unit) { + var introNextPg by remember { mutableStateOf(false) } + val (introContact, onCancelSel) = remember { mutableStateOf(contact) } + if (!introNextPg) { + Surface { + Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { + IconButton( + onClick = { setInfoDrawer(false) }, + Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically) + ) { + Icon(Icons.Filled.Close, "close make intro screen") + } + Text( + text = "Introduce " + contact.author.name + " to:", + fontSize = 16.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + HorizontalDivider() + Column(Modifier.verticalScroll(rememberScrollState())) { + for (c in contacts) { + if (c.id != contact.id) { + ContactCard(c, contact, { onCancelSel(c); introNextPg = true }, false, 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( + text = "Introduce Contacts", + fontSize = 16.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + Row(Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.SpaceAround) { + Column(Modifier.align(Alignment.CenterVertically)) { + ProfileCircle(36.dp, contact.author.id.bytes) + Text(contact.author.name, Modifier.padding(top = 4.dp), Color.White, 16.sp) + } + Icon(Icons.Filled.SwapHoriz, "swap", modifier = Modifier.size(48.dp)) + Column(Modifier.align(Alignment.CenterVertically)) { + ProfileCircle(36.dp, introContact.author.id.bytes) + Text(introContact.author.name, Modifier.padding(top = 4.dp), Color.White, 16.sp) + } + } + var introText by remember { mutableStateOf(TextFieldValue("")) } + Row(Modifier.padding(8.dp)) { + TextField( + introText, + { introText = it }, + placeholder = { Text(text = "Add a message (optional)") }, + ) + } + Row(Modifier.padding(8.dp)) { + TextButton( + onClick = { setInfoDrawer(false); introNextPg = false; }, + Modifier.fillMaxWidth() + ) { + Text("MAKE INTRODUCTION") + } + } + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt new file mode 100644 index 0000000000000000000000000000000000000000..caaf8fad7dee40cc1a51ebafd09d9aa474d4fe95 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt @@ -0,0 +1,87 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.sp + +@Composable +fun ContactDropDown( + expanded: Boolean, + isExpanded: (Boolean) -> Unit, + setInfoDrawer: (Boolean) -> Unit +) { + var connectionMode by remember { mutableStateOf(false) } + var contactMode by remember { mutableStateOf(false) } + DropdownMenu( + expanded = expanded, + onDismissRequest = { isExpanded(false) }, + ) { + DropdownMenuItem(onClick = { setInfoDrawer(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 }, + ) { + DropdownMenuItem(onClick = { false }) { + Text("Connections", 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 }, + ) { + DropdownMenuItem(onClick = { false }) { + Text("Contact", fontSize = 12.sp) + } + DropdownMenuItem(onClick = { false }) { + Text("Change contact name", fontSize = 14.sp) + } + DropdownMenuItem(onClick = { false }) { + Text("Delete contact", fontSize = 14.sp) + } + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c8ec6e06e35bb7881610792acfc00140e4a86e3 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt @@ -0,0 +1,25 @@ +package org.briarproject.briar.desktop.contact + +import androidx.compose.runtime.Composable +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.briar.desktop.contact.ContactInfoDrawerState.MakeIntro +import org.briarproject.briar.desktop.conversation.ContactDrawerMakeIntro + +// Right drawer state +enum class ContactInfoDrawerState { + MakeIntro, + ConnectBT, + ConnectRD +} + +@Composable +fun ContactInfoDrawer( + contact: Contact, + contacts: List<Contact>, + setInfoDrawer: (Boolean) -> Unit, + drawerState: ContactInfoDrawerState +) { + when (drawerState) { + MakeIntro -> ContactDrawerMakeIntro(contact, contacts, setInfoDrawer) + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt new file mode 100644 index 0000000000000000000000000000000000000000..b25092841f3418706f8a3f02687872c63f96f8a2 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt @@ -0,0 +1,61 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.briar.desktop.theme.surfaceVariant +import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH +import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE + +@Composable +fun ContactList( + contact: Contact, + contacts: List<Contact>, + onContactSelect: (Contact) -> Unit, + onContactAdd: (Boolean) -> Unit +) { + var searchValue by remember { mutableStateOf("") } + val 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(CONTACTLIST_WIDTH), + backgroundColor = MaterialTheme.colors.surfaceVariant, + topBar = { + Column( + modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp), + ) { + SearchTextField(searchValue, onValueChange = { searchValue = it }, onContactAdd) + } + }, + content = { + Column(Modifier.verticalScroll(rememberScrollState())) { + for (c in filteredContacts) { + ContactCard(c, contact, onContactSelect, contact.id == c.id, true) + } + } + }, + ) +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt similarity index 83% rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt rename to src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt index 194155a15845287818e8c7197182447571fbbfa4..9e51de185321c436107891c7c371c911423967ed 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.dialogs +package org.briarproject.briar.desktop.contact import androidx.compose.runtime.mutableStateListOf import org.briarproject.bramble.api.contact.Contact @@ -25,4 +25,8 @@ constructor( this.contacts.add(contact) } } + + fun getFirst(): Contact? { + return if (contacts.isEmpty()) null else contacts[0] + } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/Identicon.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/Identicon.kt similarity index 98% rename from src/main/kotlin/org/briarproject/briar/desktop/paul/views/Identicon.kt rename to src/main/kotlin/org/briarproject/briar/desktop/contact/Identicon.kt index 30dc74d9adfb84ac92ee978ef97b9ed0881ed40a..4adf14020435ea4de21ae5c48abf2a01528c7075 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/Identicon.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/Identicon.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.paul.views +package org.briarproject.briar.desktop.contact import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/ProfileCircle.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt similarity index 92% rename from src/main/kotlin/org/briarproject/briar/desktop/paul/views/ProfileCircle.kt rename to src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt index c821e0391e0ae0a7ff881c33be0df9e4a76762ee..e2fcbf24336dcbed7ecf24ba62fb52cb931eea22 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/ProfileCircle.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.paul.views +package org.briarproject.briar.desktop.contact import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Canvas @@ -11,7 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import org.briarproject.briar.desktop.paul.theme.outline +import org.briarproject.briar.desktop.theme.outline @Preview @Composable diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt new file mode 100644 index 0000000000000000000000000000000000000000..824ef78bc17d39a113fcf4315fc006530d0475f3 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt @@ -0,0 +1,48 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PersonAdd +import androidx.compose.material.icons.filled.Search +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onContactAdd: (Boolean) -> Unit) { + TextField( + value = searchValue, + onValueChange = onValueChange, + singleLine = true, + textStyle = TextStyle(fontSize = 16.sp, color = MaterialTheme.colors.onSurface), + placeholder = { Text("Contacts") }, + shape = RoundedCornerShape(0.dp), + leadingIcon = { + val padding = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 12.dp) + Icon(Icons.Filled.Search, "search contacts", padding) + }, + trailingIcon = { + IconButton( + onClick = { onContactAdd(true) }, + modifier = Modifier.padding(end = 10.dp).size(32.dp) + .background(MaterialTheme.colors.primary, CircleShape) + ) { + Icon(Icons.Filled.PersonAdd, "add contact", tint = Color.White, modifier = Modifier.size(20.dp)) + } + }, + modifier = Modifier.fillMaxSize() + ) +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/Chat.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Chat.kt similarity index 74% rename from src/main/kotlin/org/briarproject/briar/desktop/chat/Chat.kt rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/Chat.kt index d59cdeda91fba29968cce179e904dbd74831ad9e..9735b9968873dc9dfeae814371f82bfb4d19562e 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/chat/Chat.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Chat.kt @@ -1,6 +1,6 @@ -package org.briarproject.briar.desktop.chat +package org.briarproject.briar.desktop.conversation -import org.briarproject.briar.desktop.paul.views.getFormattedTimestamp +import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp class Chat { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/ChatHistoryConversationVisitor.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatHistoryConversationVisitor.kt similarity index 98% rename from src/main/kotlin/org/briarproject/briar/desktop/chat/ChatHistoryConversationVisitor.kt rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatHistoryConversationVisitor.kt index f8aa3835bbea1bca852b8070393882a533453f2d..18f19329c85ad64369b0b21742159346b3872b1d 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/chat/ChatHistoryConversationVisitor.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatHistoryConversationVisitor.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.chat +package org.briarproject.briar.desktop.conversation import org.briarproject.bramble.api.db.DbException import org.briarproject.bramble.util.LogUtils diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatState.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatState.kt new file mode 100644 index 0000000000000000000000000000000000000000..316df7dc3116c7ea805560acb00130f50ebff4fd --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatState.kt @@ -0,0 +1,37 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import org.briarproject.bramble.api.contact.ContactId +import org.briarproject.briar.api.conversation.ConversationMessageHeader +import org.briarproject.briar.desktop.CVM +import org.briarproject.briar.desktop.MM +import org.briarproject.briar.desktop.ui.UiState +import java.util.Collections + +@Composable +fun ChatState(id: ContactId): MutableState<UiState<Chat>> { + val state: MutableState<UiState<Chat>> = remember { mutableStateOf(UiState.Loading) } + val messagingManager = MM.current + val conversationManager = CVM.current + + DisposableEffect(id) { + state.value = UiState.Loading + val chat = Chat() + val visitor = ChatHistoryConversationVisitor(chat, messagingManager) + val messageHeaders: List<ConversationMessageHeader> = ArrayList(conversationManager.getMessageHeaders(id)) + Collections.sort(messageHeaders, ConversationMessageHeaderComparator()) + // Reverse order here because we're using reverseLayout=true on the LazyColumn to display items + // from bottom to top + Collections.reverse(messageHeaders) + for (header in messageHeaders) { + header.accept(visitor) + } + state.value = UiState.Success(chat) + onDispose { } + } + return state +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt new file mode 100644 index 0000000000000000000000000000000000000000..92c5988d8158b8375ec8a3ca1198a3ba78256a70 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt @@ -0,0 +1,78 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.briar.desktop.contact.ContactInfoDrawer +import org.briarproject.briar.desktop.contact.ContactInfoDrawerState +import org.briarproject.briar.desktop.theme.surfaceVariant +import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH +import org.briarproject.briar.desktop.views.SIDEBAR_WIDTH + +@Composable +fun Conversation( + contact: 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 = { ConversationHeader(contact, expanded, setExpanded, setInfoDrawer) }, + content = { padding -> + Box(modifier = Modifier.padding(padding)) { + val chat = ChatState(contact.id) + TextBubbles(chat.value) + } + }, + bottomBar = { ConversationInput() }, + ) + if (infoDrawer) { + // TODO Find non-hacky way of setting scrim on entire app + Box( + Modifier.offset(-(CONTACTLIST_WIDTH + SIDEBAR_WIDTH)) + .requiredSize(maxWidth + CONTACTLIST_WIDTH + SIDEBAR_WIDTH, maxHeight) + .background(Color(0, 0, 0, 100)) + .clickable( + // prevent visual indication + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { setInfoDrawer(false) } + ) + Column( + modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH) + .offset(maxWidth + animatedInfoDrawerOffsetX) + .background( + MaterialTheme.colors.surfaceVariant, + RoundedCornerShape(topStart = 10.dp, bottomStart = 10.dp) + ) + ) { + ContactInfoDrawer(contact, contacts, setInfoDrawer, drawerState) + } + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt new file mode 100644 index 0000000000000000000000000000000000000000..0372dce4d2e517c30aabefc0bc1bddf211bc6d99 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt @@ -0,0 +1,66 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.briar.desktop.contact.ProfileCircle +import org.briarproject.briar.desktop.theme.outline +import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE + +@Composable +fun ConversationHeader( + contact: Contact, + expanded: Boolean, + isExpanded: (Boolean) -> Unit, + setInfoDrawer: (Boolean) -> Unit +) { + // TODO hook up online indicator logic + val onlineColor = MaterialTheme.colors.secondary + val outlineColor = MaterialTheme.colors.outline + + Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) { + Row(modifier = Modifier.align(Alignment.Center)) { + ProfileCircle(36.dp, contact.author.id.bytes) + Canvas( + modifier = Modifier.align(Alignment.CenterVertically), + onDraw = { + val size = 10.dp.toPx() + withTransform({ translate(left = -6f, top = 12f) }) { + drawCircle(color = outlineColor, radius = (size + 2.dp.toPx()) / 2f) + drawCircle(color = onlineColor, radius = size / 2f) + } + } + ) + Text( + contact.author.name, + 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", modifier = Modifier.size(24.dp)) + ContactDropDown(expanded, isExpanded, setInfoDrawer) + } + HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt new file mode 100644 index 0000000000000000000000000000000000000000..8864a041bc990d4f56fe9a07e406d871f04efdd7 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt @@ -0,0 +1,84 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Send +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.briarproject.briar.desktop.theme.DarkColors + +@Preview +@Composable +fun PreviewConversationInput() { + MaterialTheme(colors = DarkColors) { + Surface { + ConversationInput() + } + } +} + +@Composable +fun ConversationInput() { + var text by remember { mutableStateOf("") } + Column { + HorizontalDivider() + TextField( + value = text, + onValueChange = { text = it }, + maxLines = 10, + textStyle = TextStyle(fontSize = 16.sp, lineHeight = 16.sp), + placeholder = { Text("Message") }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(0.dp), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.background, + focusedIndicatorColor = MaterialTheme.colors.background, + unfocusedIndicatorColor = MaterialTheme.colors.background + ), + leadingIcon = { + IconButton( + onClick = {}, + Modifier.padding(4.dp).size(32.dp) + .background(MaterialTheme.colors.primary, CircleShape), + ) { + Icon(Icons.Filled.Add, "add attachment", Modifier.size(24.dp), Color.White) + } + }, + trailingIcon = { + IconButton( + onClick = { }, modifier = Modifier.padding(4.dp).size(32.dp), + ) { + Icon( + Icons.Filled.Send, + "send message", + tint = MaterialTheme.colors.secondary, + modifier = Modifier.size(24.dp) + ) + } + } + ) + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/ConversationMessageHeaderComparator.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageHeaderComparator.kt similarity index 86% rename from src/main/kotlin/org/briarproject/briar/desktop/chat/ConversationMessageHeaderComparator.kt rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageHeaderComparator.kt index a2b4b680e183ca5ec573f49661a882b649461a05..1579924735705b1baa24002a27dae53ea947595a 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/chat/ConversationMessageHeaderComparator.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageHeaderComparator.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.chat +package org.briarproject.briar.desktop.conversation import org.briarproject.briar.api.conversation.ConversationMessageHeader diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt new file mode 100644 index 0000000000000000000000000000000000000000..6ab00cf5605f0812bee06ed7ddc1da94a9a6bced --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt @@ -0,0 +1,41 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.briar.desktop.contact.ContactInfoDrawerState.MakeIntro +import org.briarproject.briar.desktop.contact.ContactsViewModel + +@Composable +fun PrivateMessageView( + contact: Contact, + contacts: ContactsViewModel, + onContactSelect: (Contact) -> Unit +) { + val (isDialogVisible, setDialogVisibility) = remember { mutableStateOf(false) } + val (dropdownExpanded, setExpanded) = remember { mutableStateOf(false) } + val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) } + val (contactDrawerState, setDrawerState) = remember { mutableStateOf(MakeIntro) } + AddContactDialog(isDialogVisible, setDialogVisibility) + Row(modifier = Modifier.fillMaxWidth()) { + ContactList(contact, contacts.contacts, onContactSelect, setDialogVisibility) + VerticalDivider() + Column(modifier = Modifier.weight(1f).fillMaxHeight()) { + Conversation( + contact, + contacts.contacts, + dropdownExpanded, + setExpanded, + infoDrawer, + setInfoDrawer, + contactDrawerState + ) + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/SimpleMessage.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/SimpleMessage.kt similarity index 77% rename from src/main/kotlin/org/briarproject/briar/desktop/chat/SimpleMessage.kt rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/SimpleMessage.kt index cf1f56a4916ba82a5a2f64d7c7c0abfc6788776d..686f1e53151c5bd51b46633ee8fc5df58e97f524 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/chat/SimpleMessage.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/SimpleMessage.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.chat +package org.briarproject.briar.desktop.conversation data class SimpleMessage( val local: Boolean, diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubble.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubble.kt new file mode 100644 index 0000000000000000000000000000000000000000..543f06ad956b7498b400234da970520bfbadeb63 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubble.kt @@ -0,0 +1,67 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DoneAll +import androidx.compose.material.icons.filled.Schedule +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.briarproject.briar.desktop.theme.awayMsgBubble +import org.briarproject.briar.desktop.theme.localMsgBubble + +@Composable +fun TextBubble(m: SimpleMessage) { + if (m.local) { + TextBubble( + m, + Alignment.End, + MaterialTheme.colors.localMsgBubble, + RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp) + ) + } else { + TextBubble( + m, + Alignment.Start, + MaterialTheme.colors.awayMsgBubble, + RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp) + ) + } +} + +@Composable +fun TextBubble(m: SimpleMessage, alignment: Alignment.Horizontal, color: Color, shape: RoundedCornerShape) { + Column(Modifier.fillMaxWidth()) { + Column(Modifier.fillMaxWidth(fraction = 0.8f).align(alignment)) { + Card(Modifier.align(alignment), backgroundColor = color, shape = shape) { + Column( + Modifier.padding(8.dp) + ) { + Text(m.message, fontSize = 14.sp, modifier = Modifier.align(Alignment.Start)) + Row(modifier = Modifier.padding(top = 4.dp)) { + Text(m.time, Modifier.padding(end = 4.dp), fontSize = 10.sp) + if (m.delivered) { + val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically) + Icon(Icons.Filled.DoneAll, "sent", modifier) + } else { + val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically) + Icon(Icons.Filled.Schedule, "sending", modifier) + } + } + } + } + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubbles.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubbles.kt new file mode 100644 index 0000000000000000000000000000000000000000..d8e157a418821fc6316cd600d67c92196c72c6bc --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubbles.kt @@ -0,0 +1,25 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import org.briarproject.briar.desktop.ui.UiState + +@Composable +fun TextBubbles(chat: UiState<Chat>) { + when (chat) { + is UiState.Loading -> Loader() + is UiState.Error -> Loader() + is UiState.Success -> + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + reverseLayout = true, + contentPadding = PaddingValues(8.dp) + ) { + items(chat.data.messages) { m -> TextBubble(m) } + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt similarity index 98% rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt rename to src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt index a61f33837cd4a0e951f45f518c39992595e0eb5c..488a225e8a53532798573f64f6b77d06ff4731f6 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.dialogs +package org.briarproject.briar.desktop.login import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt similarity index 95% rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt rename to src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt index 228a8012197763b092fd5c0e3fe213dcf0f382df..a86e37f42534991545ca1340499d7ceb96547855 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.dialogs +package org.briarproject.briar.desktop.login import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt similarity index 98% rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt rename to src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt index 7cc06944c6aa53ab3f8b69aad27363c6248750db..78758000be6c01d520982cb4c7569d856ecb5f03 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.dialogs +package org.briarproject.briar.desktop.login import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt similarity index 96% rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt rename to src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt index b5a9b016e02a2ef18ee3b383285bc749e2ea60e6..6a133b40d2e7bf54030140735869042633e57227 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.dialogs +package org.briarproject.briar.desktop.login import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt b/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt similarity index 84% rename from src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt rename to src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt index 007b84da5604bb8138c639764c9decb4677d1bb8..f91bcf5008fd3bcf48bcd400d9ce2557227a7f11 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.paul.views +package org.briarproject.briar.desktop.views import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -24,12 +24,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.IM -import org.briarproject.briar.desktop.paul.theme.sidebarSurface +import org.briarproject.briar.desktop.contact.ProfileCircle +import org.briarproject.briar.desktop.theme.sidebarSurface +import org.briarproject.briar.desktop.ui.UiMode val SIDEBAR_WIDTH = 56.dp @Composable -fun BriarSidebar(uiMode: UiModes, setUiMode: (UiModes) -> Unit) { +fun BriarSidebar(uiMode: UiMode, setUiMode: (UiMode) -> Unit) { Surface(modifier = Modifier.width(SIDEBAR_WIDTH).fillMaxHeight(), color = MaterialTheme.colors.sidebarSurface) { Column(verticalArrangement = Arrangement.Top) { IconButton( @@ -42,25 +44,25 @@ fun BriarSidebar(uiMode: UiModes, setUiMode: (UiModes) -> Unit) { BriarSidebarButton( uiMode = uiMode, setUiMode = setUiMode, - UiModes.CONTACTS, + UiMode.CONTACTS, Icons.Filled.Contacts ) BriarSidebarButton( uiMode = uiMode, setUiMode = setUiMode, - UiModes.GROUPS, + UiMode.GROUPS, Icons.Filled.Group ) BriarSidebarButton( uiMode = uiMode, setUiMode = setUiMode, - UiModes.FORUMS, + UiMode.FORUMS, Icons.Filled.Forum ) BriarSidebarButton( uiMode = uiMode, setUiMode = setUiMode, - UiModes.BLOGS, + UiMode.BLOGS, Icons.Filled.ChromeReaderMode ) } @@ -68,19 +70,19 @@ fun BriarSidebar(uiMode: UiModes, setUiMode: (UiModes) -> Unit) { BriarSidebarButton( uiMode = uiMode, setUiMode = setUiMode, - UiModes.TRANSPORTS, + UiMode.TRANSPORTS, Icons.Filled.WifiTethering ) BriarSidebarButton( uiMode = uiMode, setUiMode = setUiMode, - UiModes.SETTINGS, + UiMode.SETTINGS, Icons.Filled.Settings ) BriarSidebarButton( uiMode = uiMode, setUiMode = setUiMode, - UiModes.SIGNOUT, + UiMode.SIGNOUT, Icons.Filled.Logout ) } @@ -88,7 +90,7 @@ fun BriarSidebar(uiMode: UiModes, setUiMode: (UiModes) -> Unit) { } @Composable -fun BriarSidebarButton(uiMode: UiModes, setUiMode: (UiModes) -> Unit, thisMode: UiModes, icon: ImageVector) { +fun BriarSidebarButton(uiMode: UiMode, setUiMode: (UiMode) -> Unit, thisMode: UiMode, icon: ImageVector) { val tint = if (uiMode == thisMode) MaterialTheme.colors.primary else MaterialTheme.colors.onSurface Column { IconButton( diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentTypeList.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentTypeList.kt deleted file mode 100644 index 66f0de30cdfccc89db20096dc2a1f0875e75239a..0000000000000000000000000000000000000000 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentTypeList.kt +++ /dev/null @@ -1,16 +0,0 @@ -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") - ) -} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionTypeList.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionTypeList.kt deleted file mode 100644 index b8e60c48a9071b7d714d7d04237942f248d4e386..0000000000000000000000000000000000000000 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionTypeList.kt +++ /dev/null @@ -1,13 +0,0 @@ -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/ContentType.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt deleted file mode 100644 index 0704b6c3f174425bf5fc4ef05755a423efbddf68..0000000000000000000000000000000000000000 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.briarproject.briar.desktop.paul.model - -data class ContentType( - val id: Int, - val name: String, - // val icon: Icon, -) 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 deleted file mode 100644 index ae5b2e4d76b5da4b0cf655a4e3424342ae6e22f6..0000000000000000000000000000000000000000 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/OptionType.kt +++ /dev/null @@ -1,8 +0,0 @@ -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/views/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt deleted file mode 100644 index a05d9cae64e0d171be6911dda7ef1e62d8443634..0000000000000000000000000000000000000000 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt +++ /dev/null @@ -1,802 +0,0 @@ -package org.briarproject.briar.desktop.paul.views - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -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.verticalScroll -import androidx.compose.material.AlertDialog -import androidx.compose.material.Button -import androidx.compose.material.Card -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Divider -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.icons.Icons.Filled -import androidx.compose.material.icons.filled.Add -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 -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.withTransform -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.briarproject.bramble.api.FormatException -import org.briarproject.bramble.api.contact.Contact -import org.briarproject.bramble.api.contact.ContactId -import org.briarproject.bramble.api.contact.ContactManager -import org.briarproject.bramble.api.db.ContactExistsException -import org.briarproject.bramble.api.db.PendingContactExistsException -import org.briarproject.bramble.api.identity.AuthorConstants -import org.briarproject.bramble.util.StringUtils -import org.briarproject.briar.api.conversation.ConversationMessageHeader -import org.briarproject.briar.desktop.CTM -import org.briarproject.briar.desktop.CVM -import org.briarproject.briar.desktop.MM -import org.briarproject.briar.desktop.chat.Chat -import org.briarproject.briar.desktop.chat.ChatHistoryConversationVisitor -import org.briarproject.briar.desktop.chat.ConversationMessageHeaderComparator -import org.briarproject.briar.desktop.chat.SimpleMessage -import org.briarproject.briar.desktop.chat.UiState -import org.briarproject.briar.desktop.dialogs.ContactsViewModel -import org.briarproject.briar.desktop.paul.theme.DarkColors -import org.briarproject.briar.desktop.paul.theme.awayMsgBubble -import org.briarproject.briar.desktop.paul.theme.divider -import org.briarproject.briar.desktop.paul.theme.localMsgBubble -import org.briarproject.briar.desktop.paul.theme.outline -import org.briarproject.briar.desktop.paul.theme.selectedCard -import org.briarproject.briar.desktop.paul.theme.surfaceVariant -import java.security.GeneralSecurityException -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle -import java.time.temporal.ChronoUnit -import java.util.Collections - -val HEADER_SIZE = 56.dp -val CONTACTLIST_WIDTH = 275.dp - -// Right drawer state -enum class ContactInfoDrawerState { - MakeIntro, - ConnectBT, - ConnectRD -} - -@Composable -fun HorizontalDivider(modifier: Modifier = Modifier) { - Divider(color = MaterialTheme.colors.divider, thickness = 1.dp, modifier = modifier.fillMaxWidth()) -} - -@Composable -fun VerticalDivider() { - Divider(color = MaterialTheme.colors.divider, modifier = Modifier.fillMaxHeight().width(1.dp)) -} - -@Composable -fun PrivateMessageView( - contact: Contact, - contacts: ContactsViewModel, - onContactSelect: (Contact) -> Unit -) { - val (isDialogVisible, setDialogVisibility) = remember { mutableStateOf(false) } - val (dropdownExpanded, setExpanded) = remember { mutableStateOf(false) } - val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) } - val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) } - AddContactDialog(isDialogVisible, setDialogVisibility) - Row(modifier = Modifier.fillMaxWidth()) { - ContactList(contact, contacts.contacts, onContactSelect, setDialogVisibility) - VerticalDivider() - Column(modifier = Modifier.weight(1f).fillMaxHeight()) { - Conversation( - contact, - contacts.contacts, - dropdownExpanded, - setExpanded, - infoDrawer, - setInfoDrawer, - contactDrawerState - ) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun AddContactDialog(isVisible: Boolean, setDialogVisibility: (Boolean) -> Unit) { - if (!isVisible) { - return - } - var contactAlias by remember { mutableStateOf("") } - var contactLink by remember { mutableStateOf("") } - val contactManager = CTM.current - val ownLink = CTM.current.handshakeLink - AlertDialog( - onDismissRequest = { setDialogVisibility(false) }, - text = { - Column(modifier = Modifier.fillMaxWidth()) { - Row(Modifier.fillMaxWidth().padding(vertical = 16.dp)) { - Text( - text = "Add Contact at a Distance", - fontSize = 24.sp, - modifier = Modifier.align(Alignment.CenterVertically) - ) - } - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - Text( - "Contact's Link", - Modifier.width(128.dp).align(Alignment.CenterVertically), - ) - TextField(contactLink, onValueChange = { contactLink = it }, modifier = Modifier.fillMaxWidth()) - } - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - Text( - "Contact's Name", - Modifier.width(128.dp).align(Alignment.CenterVertically), - ) - TextField(contactAlias, onValueChange = { contactAlias = it }, modifier = Modifier.fillMaxWidth()) - } - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - Text( - "Your Link", - modifier = Modifier.width(128.dp).align(Alignment.CenterVertically), - ) - TextField( - ownLink, - onValueChange = {}, - modifier = Modifier.fillMaxWidth() - ) - } - } - }, - confirmButton = { - Button( - onClick = { - if (ownLink.equals(contactLink)) { - println("Please enter contact's link, not your own") - setDialogVisibility(false) - return@Button - } - addPendingContact(contactManager, contactAlias, contactLink) - setDialogVisibility(false) - }, - ) { - Text("Add") - } - }, - dismissButton = { - TextButton( - onClick = { setDialogVisibility(false) } - ) { - Text("Cancel", color = MaterialTheme.colors.onSurface) - } - }, - modifier = Modifier.size(600.dp, 300.dp), - ) -} - -private fun addPendingContact(contactManager: ContactManager, alias: String, link: String) { - if (aliasIsInvalid(alias)) { - println("Alias is invalid") - return - } - try { - contactManager.addPendingContact(link, alias) - } catch (e: FormatException) { - println("Link is invalid") - println(e.stackTrace) - } catch (e: GeneralSecurityException) { - println("Public key is invalid") - println(e.stackTrace) - } - /* - TODO: Warn user that the following two errors might be an attack - - Use `e.pendingContact.id.bytes` and `e.pendingContact.alias` to implement the following logic: - https://code.briarproject.org/briar/briar-gtk/-/merge_requests/97 - - */ - catch (e: ContactExistsException) { - println("Contact already exists") - println(e.stackTrace) - } catch (e: PendingContactExistsException) { - println("Pending Contact already exists") - println(e.stackTrace) - } -} - -private fun aliasIsInvalid(alias: String): Boolean { - val aliasUtf8 = StringUtils.toUtf8(alias) - return aliasUtf8.isEmpty() || aliasUtf8.size > AuthorConstants.MAX_AUTHOR_NAME_LENGTH -} - -@Composable -fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onContactAdd: (Boolean) -> Unit) { - TextField( - value = searchValue, - onValueChange = onValueChange, - singleLine = true, - textStyle = TextStyle(fontSize = 16.sp, color = MaterialTheme.colors.onSurface), - placeholder = { Text("Contacts") }, - shape = RoundedCornerShape(0.dp), - leadingIcon = { - val padding = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 12.dp) - Icon(Filled.Search, "search contacts", padding) - }, - trailingIcon = { - IconButton( - onClick = { onContactAdd(true) }, - modifier = Modifier.padding(end = 10.dp).size(32.dp) - .background(MaterialTheme.colors.primary, CircleShape) - ) { - Icon(Filled.PersonAdd, "add contact", tint = Color.White, modifier = Modifier.size(20.dp)) - } - }, - modifier = Modifier.fillMaxSize() - ) -} - -@Composable -fun ContactCard( - contact: Contact, - selContact: Contact, - onSel: (Contact) -> Unit, - selected: Boolean, - drawNotifications: Boolean -) { - val bgColor = if (selected) MaterialTheme.colors.selectedCard else MaterialTheme.colors.surfaceVariant - val outlineColor = MaterialTheme.colors.outline - val briarPrimary = MaterialTheme.colors.primary - val briarSecondary = MaterialTheme.colors.secondary - val briarSurfaceVar = MaterialTheme.colors.surfaceVariant - - Card( - modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).clickable(onClick = { onSel(contact) }), - shape = RoundedCornerShape(0.dp), - backgroundColor = bgColor, - contentColor = MaterialTheme.colors.onSurface - ) { - Row(horizontalArrangement = Arrangement.SpaceBetween) { - Row(modifier = Modifier.align(Alignment.CenterVertically).padding(horizontal = 16.dp)) { - // TODO Pull profile pictures - ProfileCircle(36.dp, contact.author.id.bytes) - // Draw notification badges - if (drawNotifications) { - Canvas( - modifier = Modifier.align(Alignment.CenterVertically), - onDraw = { - val size = 10.dp.toPx() - withTransform({ translate(left = -6f, top = -12f) }) { - drawCircle(color = outlineColor, radius = (size + 2.dp.toPx()) / 2f) - drawCircle(color = briarPrimary, radius = size / 2f) - } - } - ) - } - Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp)) { - Text( - contact.author.name, - fontSize = 14.sp, - modifier = Modifier.align(Alignment.Start).padding(bottom = 2.dp) - ) - val latestMsgTime = CVM.current.getGroupCount(contact.id).latestMsgTime - Text( - getFormattedTimestamp(latestMsgTime), - fontSize = 10.sp, - modifier = Modifier.align(Alignment.Start) - ) - } - } - Canvas( - modifier = Modifier.padding(start = 32.dp, end = 18.dp).size(22.dp).align(Alignment.CenterVertically), - onDraw = { - val size = 16.dp.toPx() - drawCircle(color = outlineColor, radius = size / 2f) - // TODO check if contact online - if (true) { - drawCircle(color = briarSecondary, radius = 14.dp.toPx() / 2f) - } else { - drawCircle(color = briarSurfaceVar, radius = 14.dp.toPx() / 2f) - } - } - ) - } - } - HorizontalDivider() -} - -fun getFormattedTimestamp(timestamp: Long): String { - val messageTime = LocalDateTime.ofInstant( - Instant.ofEpochMilli(timestamp), ZoneId.systemDefault() - ) - val currentTime = LocalDateTime.now() - val difference = ChronoUnit.MINUTES.between(messageTime, currentTime) - - val formatter = if (difference < 1440) { // = 1 day - DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - } else { - DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) - } - return messageTime.format(formatter) -} - -@Composable -fun ContactList( - contact: Contact, - contacts: List<Contact>, - onContactSelect: (Contact) -> Unit, - onContactAdd: (Boolean) -> Unit -) { - var searchValue by remember { mutableStateOf("") } - val 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(CONTACTLIST_WIDTH), - backgroundColor = MaterialTheme.colors.surfaceVariant, - topBar = { - Column( - modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp), - ) { - SearchTextField(searchValue, onValueChange = { searchValue = it }, onContactAdd) - } - }, - content = { - Column(Modifier.verticalScroll(rememberScrollState())) { - for (c in filteredContacts) { - ContactCard(c, contact, onContactSelect, contact.id == c.id, true) - } - } - }, - ) -} - -@Composable -fun TextBubble(m: SimpleMessage) { - if (m.local) { - TextBubble( - m, - Alignment.End, - MaterialTheme.colors.localMsgBubble, - RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp) - ) - } else { - TextBubble( - m, - Alignment.Start, - MaterialTheme.colors.awayMsgBubble, - RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp) - ) - } -} - -@Composable -fun TextBubble(m: SimpleMessage, alignment: Alignment.Horizontal, color: Color, shape: RoundedCornerShape) { - Column(Modifier.fillMaxWidth()) { - Column(Modifier.fillMaxWidth(fraction = 0.8f).align(alignment)) { - Card(Modifier.align(alignment), backgroundColor = color, shape = shape) { - Column( - Modifier.padding(8.dp) - ) { - Text(m.message, fontSize = 14.sp, modifier = Modifier.align(Alignment.Start)) - Row(modifier = Modifier.padding(top = 4.dp)) { - Text(m.time, Modifier.padding(end = 4.dp), fontSize = 10.sp) - if (m.delivered) { - val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically) - Icon(Filled.DoneAll, "sent", modifier) - } else { - val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically) - Icon(Filled.Schedule, "sending", modifier) - } - } - } - } - } - } -} - -@Composable -fun DrawTextBubbles(chat: UiState<Chat>) { - when (chat) { - is UiState.Loading -> Loader() - is UiState.Error -> Loader() - is UiState.Success -> - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), - reverseLayout = true, - contentPadding = PaddingValues(8.dp) - ) { - items(chat.data.messages) { m -> TextBubble(m) } - } - } -} - -@Composable -fun Loader() { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth().padding(20.dp) - ) { - CircularProgressIndicator() - } -} - -@Composable -fun ContactDropDown( - expanded: Boolean, - isExpanded: (Boolean) -> Unit, - setInfoDrawer: (Boolean) -> Unit -) { - var connectionMode by remember { mutableStateOf(false) } - var contactMode by remember { mutableStateOf(false) } - DropdownMenu( - expanded = expanded, - onDismissRequest = { isExpanded(false) }, - ) { - DropdownMenuItem(onClick = { setInfoDrawer(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(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(Filled.ArrowRight, "connections", modifier = Modifier.align(Alignment.CenterVertically)) - } - } - } - if (connectionMode) { - DropdownMenu( - expanded = connectionMode, - onDismissRequest = { connectionMode = false }, - ) { - DropdownMenuItem(onClick = { false }) { - Text("Connections", 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 }, - ) { - DropdownMenuItem(onClick = { false }) { - Text("Contact", fontSize = 12.sp) - } - DropdownMenuItem(onClick = { false }) { - Text("Change contact name", fontSize = 14.sp) - } - DropdownMenuItem(onClick = { false }) { - Text("Delete contact", fontSize = 14.sp) - } - } - } -} - -@Composable -fun ConversationHeader( - contact: Contact, - expanded: Boolean, - isExpanded: (Boolean) -> Unit, - setInfoDrawer: (Boolean) -> Unit -) { - // TODO hook up online indicator logic - val onlineColor = MaterialTheme.colors.secondary - val outlineColor = MaterialTheme.colors.outline - - Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) { - Row(modifier = Modifier.align(Alignment.Center)) { - ProfileCircle(36.dp, contact.author.id.bytes) - Canvas( - modifier = Modifier.align(Alignment.CenterVertically), - onDraw = { - val size = 10.dp.toPx() - withTransform({ translate(left = -6f, top = 12f) }) { - drawCircle(color = outlineColor, radius = (size + 2.dp.toPx()) / 2f) - drawCircle(color = onlineColor, radius = size / 2f) - } - } - ) - Text( - contact.author.name, - 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(Filled.MoreVert, "contact info", modifier = Modifier.size(24.dp)) - ContactDropDown(expanded, isExpanded, setInfoDrawer) - } - HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) - } -} - -@Composable -fun ConversationInput() { - var text by remember { mutableStateOf("") } - Column { - HorizontalDivider() - TextField( - value = text, - onValueChange = { text = it }, - maxLines = 10, - textStyle = TextStyle(fontSize = 16.sp, lineHeight = 16.sp), - placeholder = { Text("Message") }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(0.dp), - colors = TextFieldDefaults.textFieldColors( - backgroundColor = MaterialTheme.colors.background, - focusedIndicatorColor = MaterialTheme.colors.background, - unfocusedIndicatorColor = MaterialTheme.colors.background - ), - leadingIcon = { - IconButton( - onClick = {}, - Modifier.padding(4.dp).size(32.dp) - .background(MaterialTheme.colors.primary, CircleShape), - ) { - Icon(Filled.Add, "add attachment", Modifier.size(24.dp), Color.White) - } - }, - trailingIcon = { - IconButton( - onClick = { }, modifier = Modifier.padding(4.dp).size(32.dp), - ) { - Icon( - Filled.Send, - "send message", - tint = MaterialTheme.colors.secondary, - modifier = Modifier.size(24.dp) - ) - } - } - ) - } -} - -@Preview -@Composable -fun PreviewMsgInput() { - MaterialTheme(colors = DarkColors) { - Surface { - ConversationInput() - } - } -} - -@Composable -fun ContactDrawerMakeIntro(contact: Contact, contacts: List<Contact>, setInfoDrawer: (Boolean) -> Unit) { - var introNextPg by remember { mutableStateOf(false) } - val (introContact, onCancelSel) = remember { mutableStateOf(contact) } - if (!introNextPg) { - Surface { - Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { - IconButton( - onClick = { setInfoDrawer(false) }, - Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically) - ) { - Icon(Filled.Close, "close make intro screen") - } - Text( - text = "Introduce " + contact.author.name + " to:", - fontSize = 16.sp, - modifier = Modifier.align(Alignment.CenterVertically) - ) - } - HorizontalDivider() - Column(Modifier.verticalScroll(rememberScrollState())) { - for (c in contacts) { - if (c.id != contact.id) { - ContactCard(c, contact, { onCancelSel(c); introNextPg = true }, false, 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(Filled.ArrowBack, "go back to make intro contact screen", tint = Color.White) - } - Text( - text = "Introduce Contacts", - fontSize = 16.sp, - modifier = Modifier.align(Alignment.CenterVertically) - ) - } - Row(Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.SpaceAround) { - Column(Modifier.align(Alignment.CenterVertically)) { - ProfileCircle(36.dp, contact.author.id.bytes) - Text(contact.author.name, Modifier.padding(top = 4.dp), Color.White, 16.sp) - } - Icon(Filled.SwapHoriz, "swap", modifier = Modifier.size(48.dp)) - Column(Modifier.align(Alignment.CenterVertically)) { - ProfileCircle(36.dp, introContact.author.id.bytes) - Text(introContact.author.name, Modifier.padding(top = 4.dp), Color.White, 16.sp) - } - } - var introText by remember { mutableStateOf(TextFieldValue("")) } - Row(Modifier.padding(8.dp)) { - TextField( - introText, - { introText = it }, - placeholder = { Text(text = "Add a message (optional)") }, - ) - } - Row(Modifier.padding(8.dp)) { - TextButton( - onClick = { setInfoDrawer(false); introNextPg = false; }, - Modifier.fillMaxWidth() - ) { - Text("MAKE INTRODUCTION") - } - } - } - } -} - -@Composable -fun ContactInfoDrawer( - contact: Contact, - contacts: List<Contact>, - setInfoDrawer: (Boolean) -> Unit, - drawerState: ContactInfoDrawerState -) { - when (drawerState) { - ContactInfoDrawerState.MakeIntro -> ContactDrawerMakeIntro(contact, contacts, setInfoDrawer) - } -} - -@Composable -fun Conversation( - contact: 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 = { ConversationHeader(contact, expanded, setExpanded, setInfoDrawer) }, - content = { padding -> - Box(modifier = Modifier.padding(padding)) { - val chat = ChatState(contact.id) - DrawTextBubbles(chat.value) - } - }, - bottomBar = { ConversationInput() }, - ) - if (infoDrawer) { - // TODO Find non-hacky way of setting scrim on entire app - Box( - Modifier.offset(-(CONTACTLIST_WIDTH + SIDEBAR_WIDTH)) - .requiredSize(maxWidth + CONTACTLIST_WIDTH + SIDEBAR_WIDTH, maxHeight) - .background(Color(0, 0, 0, 100)) - .clickable( - // prevent visual indication - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { setInfoDrawer(false) } - ) - Column( - modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH) - .offset(maxWidth + animatedInfoDrawerOffsetX) - .background( - MaterialTheme.colors.surfaceVariant, - RoundedCornerShape(topStart = 10.dp, bottomStart = 10.dp) - ) - ) { - ContactInfoDrawer(contact, contacts, setInfoDrawer, drawerState) - } - } - } -} - -@Composable -fun ChatState(id: ContactId): MutableState<UiState<Chat>> { - val state: MutableState<UiState<Chat>> = remember { mutableStateOf(UiState.Loading) } - val messagingManager = MM.current - val conversationManager = CVM.current - - DisposableEffect(id) { - state.value = UiState.Loading - val chat = Chat() - val visitor = ChatHistoryConversationVisitor(chat, messagingManager) - val messageHeaders: List<ConversationMessageHeader> = ArrayList(conversationManager.getMessageHeaders(id)) - Collections.sort(messageHeaders, ConversationMessageHeaderComparator()) - // Reverse order here because we're using reverseLayout=true on the LazyColumn to display items - // from bottom to top - Collections.reverse(messageHeaders) - for (header in messageHeaders) { - header.accept(visitor) - } - state.value = UiState.Success(chat) - onDispose { } - } - return state -} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/PlaceHolderSettingsView.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/PlaceHolderSettingsView.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ad692bb97924fc74714c15ccefda2344db6836d --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/PlaceHolderSettingsView.kt @@ -0,0 +1,20 @@ +package org.briarproject.briar.desktop.settings + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Button +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun PlaceHolderSettingsView(isDark: Boolean, setDark: (Boolean) -> Unit) { + Surface(Modifier.fillMaxSize()) { + Box { + Button(onClick = { setDark(!isDark) }) { + Text("Change Theme") + } + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Colors.kt b/src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt similarity index 92% rename from src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Colors.kt rename to src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt index ec1908c844e0ef3586f1b2c37656b409b0d64ca8..f3fa146562a1680be413e2ea10c92029df4db076 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Colors.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.paul.theme +package org.briarproject.briar.desktop.theme import androidx.compose.ui.graphics.Color diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Theme.kt b/src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt similarity index 97% rename from src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Theme.kt rename to src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt index 48c517b85cdbc1cab7b0e01c33601ef49a32488b..d04411c20142f0cb2fdfa1fdfdbe6d732a8e4c62 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Theme.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.paul.theme +package org.briarproject.briar.desktop.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.Colors diff --git a/src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt similarity index 89% rename from src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt rename to src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index 52f9fa36d23b04765842638344bb7e4ade6d588a..e44791b91f7f92659bd76398f45a43e778543f6c 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -15,13 +15,13 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.messaging.MessagingManager -import org.briarproject.briar.desktop.dialogs.ContactsViewModel -import org.briarproject.briar.desktop.dialogs.Login -import org.briarproject.briar.desktop.dialogs.LoginViewModel -import org.briarproject.briar.desktop.dialogs.Registration -import org.briarproject.briar.desktop.dialogs.RegistrationViewModel -import org.briarproject.briar.desktop.paul.theme.BriarTheme -import org.briarproject.briar.desktop.paul.views.BriarUIStateManager +import org.briarproject.briar.desktop.contact.ContactsViewModel +import org.briarproject.briar.desktop.login.Login +import org.briarproject.briar.desktop.login.LoginViewModel +import org.briarproject.briar.desktop.login.Registration +import org.briarproject.briar.desktop.login.RegistrationViewModel +import org.briarproject.briar.desktop.theme.BriarTheme +import org.briarproject.briar.desktop.ui.MainScreen import java.awt.Dimension import java.util.logging.Logger import javax.annotation.concurrent.Immutable @@ -116,7 +116,7 @@ constructor( MM provides messagingManager, IM provides identityManager, ) { - BriarUIStateManager(contactsViewModel, isDark, setDark) + MainScreen(contactsViewModel, isDark, setDark) } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc533cbe859a56cfdc0459a0bc26b28e8dde0f3c --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt @@ -0,0 +1,9 @@ +package org.briarproject.briar.desktop.ui + +import androidx.compose.ui.unit.dp + +object Constants { + + val HEADER_SIZE = 56.dp + val CONTACTLIST_WIDTH = 275.dp +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/HorizontalDivider.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/HorizontalDivider.kt new file mode 100644 index 0000000000000000000000000000000000000000..eca9e54199977bc2b51a5f911c773d28375f3cab --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/HorizontalDivider.kt @@ -0,0 +1,14 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.briarproject.briar.desktop.theme.divider + +@Composable +fun HorizontalDivider(modifier: Modifier = Modifier) { + Divider(color = MaterialTheme.colors.divider, thickness = 1.dp, modifier = modifier.fillMaxWidth()) +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt new file mode 100644 index 0000000000000000000000000000000000000000..95323440b85d61a0ebca97f48f17be5e0081a7ae --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt @@ -0,0 +1,20 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun Loader() { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().padding(20.dp) + ) { + CircularProgressIndicator() + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt similarity index 50% rename from src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt rename to src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt index d8699185fe69987793dbac0c4a821bb7c4babe4d..deb9801ddf592a0b488bc5959a81d9bcecd940c4 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt @@ -1,10 +1,8 @@ -package org.briarproject.briar.desktop.paul.views +package org.briarproject.briar.desktop.ui 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.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text @@ -12,18 +10,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import org.briarproject.bramble.api.contact.Contact -import org.briarproject.briar.desktop.dialogs.ContactsViewModel - -enum class UiModes { - CONTACTS, - GROUPS, - FORUMS, - BLOGS, - TRANSPORTS, - SETTINGS, - SIGNOUT -} +import org.briarproject.briar.desktop.contact.ContactsViewModel +import org.briarproject.briar.desktop.conversation.PrivateMessageView +import org.briarproject.briar.desktop.conversation.VerticalDivider +import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView +import org.briarproject.briar.desktop.views.BriarSidebar /* * This is the root of the tree, all state is held here and passed down to stateless composables, which render the UI @@ -31,55 +22,30 @@ enum class UiModes { * Multiplatform, stateless, composable are found in briarCompose (possible briar-compose project in the future) */ @Composable -fun BriarUIStateManager( +fun MainScreen( contactsViewModel: ContactsViewModel, isDark: Boolean, setDark: (Boolean) -> Unit ) { // current selected mode, changed using the sidebar buttons - val (uiMode, setUiMode) = remember { mutableStateOf(UiModes.CONTACTS) } + val (uiMode, setUiMode) = remember { mutableStateOf(UiMode.CONTACTS) } // TODO Figure out how to handle accounts with 0 contacts // current selected contact - val (contact, setContact) = remember { mutableStateOf(contact(contactsViewModel)) } - // current selected private group - val (group, setGroup) = remember { mutableStateOf(contact(contactsViewModel)) } - // current selected forum - val (forum, setForum) = remember { mutableStateOf(0) } - // current blog state - val (blog, setBlog) = remember { mutableStateOf(0) } - // current transport state - val (transport, setTransport) = remember { mutableStateOf(0) } - // current settings state - val (setting, setSetting) = remember { mutableStateOf(0) } + val (contact, setContact) = remember { mutableStateOf(contactsViewModel.getFirst()) } // Other global state that we need to track should go here also Row { BriarSidebar(uiMode, setUiMode) VerticalDivider() when (uiMode) { - UiModes.CONTACTS -> if (contact != null) PrivateMessageView( + UiMode.CONTACTS -> if (contact != null) PrivateMessageView( contact, contactsViewModel, setContact ) - UiModes.SETTINGS -> PlaceHolderSettingsView(isDark, setDark) + UiMode.SETTINGS -> PlaceHolderSettingsView(isDark, setDark) else -> Surface(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)) { Text("TBD") } } } } - -@Composable -fun PlaceHolderSettingsView(isDark: Boolean, setDark: (Boolean) -> Unit) { - Surface(Modifier.fillMaxSize()) { - Box { - Button(onClick = { setDark(!isDark) }) { - Text("Change Theme") - } - } - } -} - -fun contact(contacts: ContactsViewModel): Contact? { - return if (contacts.contacts.isEmpty()) null else contacts.contacts[0] -} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/UiMode.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiMode.kt new file mode 100644 index 0000000000000000000000000000000000000000..d30b74def9e28fe071329a2c966ddbdc48852960 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiMode.kt @@ -0,0 +1,11 @@ +package org.briarproject.briar.desktop.ui + +enum class UiMode { + CONTACTS, + GROUPS, + FORUMS, + BLOGS, + TRANSPORTS, + SETTINGS, + SIGNOUT +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/UiState.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiState.kt similarity index 81% rename from src/main/kotlin/org/briarproject/briar/desktop/chat/UiState.kt rename to src/main/kotlin/org/briarproject/briar/desktop/ui/UiState.kt index 996727a4f146ebc8029d8b2ba8ce868642d03883..9c6d6a324e30c93a814c4f6c88f2a1adf626c803 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/chat/UiState.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiState.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.chat +package org.briarproject.briar.desktop.ui sealed class UiState<out T> { object Loading : UiState<Nothing>() diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticalDivider.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticalDivider.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc710e643412878a05e67b391448c0005ceea192 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticalDivider.kt @@ -0,0 +1,15 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.width +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.briarproject.briar.desktop.theme.divider + +@Composable +fun VerticalDivider() { + Divider(color = MaterialTheme.colors.divider, modifier = Modifier.fillMaxHeight().width(1.dp)) +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/FileUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/FileUtils.kt similarity index 95% rename from src/main/kotlin/org/briarproject/briar/desktop/FileUtils.kt rename to src/main/kotlin/org/briarproject/briar/desktop/utils/FileUtils.kt index f9aa4fa9126ee97cb0b0ffc733f3f34f3322d8a0..2556737721a12440518163fc4d651935f7cf7081 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/FileUtils.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/FileUtils.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop +package org.briarproject.briar.desktop.utils import org.briarproject.bramble.util.OsUtils import java.nio.file.Files diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/TimeUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/TimeUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..f59e52bd5f58df7513d11428a5e0476d5e58aaad --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/TimeUtils.kt @@ -0,0 +1,26 @@ +package org.briarproject.briar.desktop.utils + +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.time.temporal.ChronoUnit + +object TimeUtils { + + fun getFormattedTimestamp(timestamp: Long): String { + val messageTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli(timestamp), ZoneId.systemDefault() + ) + val currentTime = LocalDateTime.now() + val difference = ChronoUnit.MINUTES.between(messageTime, currentTime) + + val formatter = if (difference < 1440) { // = 1 day + DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) + } else { + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) + } + return messageTime.format(formatter) + } +} diff --git a/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt b/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt index e3375e8483d8f4f6e302b7cfc8c36f60a0dd0f49..681e3c9b9470ddfeb6655ec81441bf733905ba59 100644 --- a/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt +++ b/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.application import org.briarproject.bramble.BrambleCoreEagerSingletons import org.briarproject.briar.BriarCoreEagerSingletons +import org.briarproject.briar.desktop.utils.FileUtils import java.io.IOException import java.nio.file.Files import java.nio.file.Path