diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt deleted file mode 100644 index 2d2f5ae5b0d2c8485a73f838a60de3ce0ce4f911..0000000000000000000000000000000000000000 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt +++ /dev/null @@ -1,142 +0,0 @@ -package org.briarproject.briar.desktop.contact - -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.ui.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 index e66b88535187b55b52547fe3f5332b820e7bf145..3a15dc7a06173669c4c87aabbf9a4ef7d6a9a68c 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt @@ -19,22 +19,18 @@ 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.theme.outline import org.briarproject.briar.desktop.theme.selectedCard import org.briarproject.briar.desktop.theme.surfaceVariant -import org.briarproject.briar.desktop.ui.CVM import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp @Composable fun ContactCard( - contact: Contact, - selContact: Contact, - onSel: (Contact) -> Unit, + contactItem: ContactItem, + onSel: () -> Unit, selected: Boolean, - drawNotifications: Boolean ) { val bgColor = if (selected) MaterialTheme.colors.selectedCard else MaterialTheme.colors.surfaceVariant val outlineColor = MaterialTheme.colors.outline @@ -43,7 +39,7 @@ fun ContactCard( val briarSurfaceVar = MaterialTheme.colors.surfaceVariant Card( - modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).clickable(onClick = { onSel(contact) }), + modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).clickable(onClick = onSel), shape = RoundedCornerShape(0.dp), backgroundColor = bgColor, contentColor = MaterialTheme.colors.onSurface @@ -51,9 +47,9 @@ fun ContactCard( 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) + ProfileCircle(36.dp, contactItem.contact.author.id.bytes) // Draw notification badges - if (drawNotifications) { + if (contactItem.unread > 0) { Canvas( modifier = Modifier.align(Alignment.CenterVertically), onDraw = { @@ -67,13 +63,12 @@ fun ContactCard( } Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp)) { Text( - contact.author.name, + contactItem.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), + if (contactItem.isEmpty) "No messages." else getFormattedTimestamp(contactItem.timestamp), fontSize = 10.sp, modifier = Modifier.align(Alignment.Start) ) @@ -82,14 +77,12 @@ fun ContactCard( 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) - } + val size = 16.dp + drawCircle(color = outlineColor, radius = size.toPx() / 2f) + drawCircle( + color = if (contactItem.isConnected) briarSecondary else briarSurfaceVar, + radius = (size - 2.dp).toPx() / 2f + ) } ) } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt index 9740a14c63c3154b05b92580b51069e55d12b489..70c73a89f615bad46840c163eb99d03bb6b696cf 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.unit.sp fun ContactDropDown( expanded: Boolean, isExpanded: (Boolean) -> Unit, - setInfoDrawer: (Boolean) -> Unit + onMakeIntroduction: () -> Unit, ) { var connectionMode by remember { mutableStateOf(false) } var contactMode by remember { mutableStateOf(false) } @@ -30,7 +30,7 @@ fun ContactDropDown( expanded = expanded, onDismissRequest = { isExpanded(false) }, ) { - DropdownMenuItem(onClick = { setInfoDrawer(true); isExpanded(false) }) { + DropdownMenuItem(onClick = { isExpanded(false); onMakeIntroduction() }) { Text("Make Introduction", fontSize = 14.sp) } DropdownMenuItem(onClick = {}) { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt index 99f247b2d9995ae95e7568f56d8a6d3062b2e0fb..f43736a975bd38b7427f2e45fc0ba9782d447131 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt @@ -1,8 +1,9 @@ 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.introduction.ContactDrawerMakeIntro +import org.briarproject.briar.desktop.introduction.IntroductionViewModel // Right drawer state enum class ContactInfoDrawerState { @@ -13,12 +14,11 @@ enum class ContactInfoDrawerState { @Composable fun ContactInfoDrawer( - contact: Contact, - contacts: List<Contact>, + introductionViewModel: IntroductionViewModel, setInfoDrawer: (Boolean) -> Unit, drawerState: ContactInfoDrawerState ) { when (drawerState) { - MakeIntro -> ContactDrawerMakeIntro(contact, contacts, setInfoDrawer) + MakeIntro -> ContactDrawerMakeIntro(introductionViewModel, setInfoDrawer) } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..7d18451058b97330b16cdc3074fa6d918c3a496b --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt @@ -0,0 +1,43 @@ +package org.briarproject.briar.desktop.contact + +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.briar.api.client.MessageTracker +import org.briarproject.briar.api.conversation.ConversationMessageHeader +import kotlin.math.max + +data class ContactItem( + val contact: Contact, + val isConnected: Boolean, + val isEmpty: Boolean, + val unread: Int, + val timestamp: Long +) { + + constructor(contact: Contact, isConnected: Boolean, groupCount: MessageTracker.GroupCount) : + this( + contact, isConnected, + isEmpty = groupCount.msgCount == 0, + unread = groupCount.unreadCount, + timestamp = groupCount.latestMsgTime + ) + + fun updateFromMessageHeader(h: ConversationMessageHeader): ContactItem { + return copy( + isEmpty = false, + unread = if (h.isRead) unread else unread + 1, + timestamp = max(h.timestamp, timestamp) + ) + } + + fun updateIsConnected(c: Boolean): ContactItem { + return copy(isConnected = c) + } + + fun updateAlias(a: String?): ContactItem { + return copy(contact = contact.updateAlias(a)) + } + + private fun Contact.updateAlias(a: String?): Contact { + return Contact(id, author, localAuthorId, a, handshakePublicKey, isVerified) + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt index ef1d1a5a112f1c8f3f7e048df4c300557fea29e7..7fa969a5b36d8be169920ee6e3b99c961f6c1ee7 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt @@ -5,8 +5,8 @@ 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.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable @@ -16,30 +16,19 @@ 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.contact.add.remote.AddContactDialog +import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel 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 + viewModel: ContactListViewModel, + addContactViewModel: AddContactViewModel, ) { - 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 - } + var isContactDialogVisible by remember { mutableStateOf(false) } + if (isContactDialogVisible) AddContactDialog(addContactViewModel) { isContactDialogVisible = false } Scaffold( modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH), backgroundColor = MaterialTheme.colors.surfaceVariant, @@ -47,13 +36,21 @@ fun ContactList( Column( modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp), ) { - SearchTextField(searchValue, onValueChange = { searchValue = it }, onContactAdd) + SearchTextField( + viewModel.filterBy.value, + onValueChange = viewModel::setFilterBy, + onContactAdd = { isContactDialogVisible = true } + ) } }, content = { - Column(Modifier.verticalScroll(rememberScrollState())) { - for (c in filteredContacts) { - ContactCard(c, contact, onContactSelect, contact.id == c.id, true) + LazyColumn { + items(viewModel.contactList) { contactItem -> + ContactCard( + contactItem, + { viewModel.selectContact(contactItem.contact) }, + viewModel.isSelected(contactItem.contact) + ) } } }, diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..ee4a7f8c82a3cb0c8a86c53c409169e8f54193bb --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt @@ -0,0 +1,73 @@ +package org.briarproject.briar.desktop.contact + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import org.briarproject.bramble.api.connection.ConnectionRegistry +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.bramble.api.contact.ContactManager +import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent +import org.briarproject.bramble.api.event.Event +import org.briarproject.bramble.api.event.EventBus +import org.briarproject.briar.api.conversation.ConversationManager +import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent +import java.util.logging.Logger +import javax.inject.Inject + +class ContactListViewModel +@Inject +constructor( + contactManager: ContactManager, + conversationManager: ConversationManager, + connectionRegistry: ConnectionRegistry, + eventBus: EventBus, +) : ContactsViewModel(contactManager, conversationManager, connectionRegistry) { + + companion object { + private val LOG = Logger.getLogger(ContactListViewModel::class.java.name) + } + + init { + // todo: where/when to remove listener again? + eventBus.addListener(this) + } + + private val _filterBy = mutableStateOf("") + private val _selectedContact = mutableStateOf<Contact?>(null) + + val filterBy: State<String> = _filterBy + val selectedContact: State<Contact?> = _selectedContact + + fun selectContact(contact: Contact) { + _selectedContact.value = contact + } + + fun isSelected(contact: Contact) = _selectedContact.value?.id == contact.id + + override fun filterContact(contact: Contact) = + // todo: also filter on alias + contact.author.name.contains(_filterBy.value, ignoreCase = true) + + fun setFilterBy(filter: String) { + _filterBy.value = filter + updateFilteredList() + } + + override fun updateFilteredList() { + super.updateFilteredList() + _selectedContact.value?.let { if (!filterContact(it)) _selectedContact.value = null } + } + + override fun eventOccurred(e: Event?) { + super.eventOccurred(e) + when (e) { + is ConversationMessageReceivedEvent<*> -> { + LOG.info("Conversation message received, updating item") + updateItem(e.contactId) { it.updateFromMessageHeader(e.messageHeader) } + } + // is AvatarUpdatedEvent -> {} + is ContactAliasChangedEvent -> { + updateItem(e.contactId) { it.updateAlias(e.alias) } + } + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt index 9e51de185321c436107891c7c371c911423967ed..ccb10ccd338ccd31c237edff5b1088034771c7bb 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt @@ -1,32 +1,90 @@ package org.briarproject.briar.desktop.contact import androidx.compose.runtime.mutableStateListOf +import org.briarproject.bramble.api.connection.ConnectionRegistry 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.contact.event.ContactAddedEvent +import org.briarproject.bramble.api.contact.event.ContactRemovedEvent +import org.briarproject.bramble.api.event.Event +import org.briarproject.bramble.api.event.EventListener +import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent +import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent +import org.briarproject.briar.api.conversation.ConversationManager +import org.briarproject.briar.desktop.utils.removeFirst +import org.briarproject.briar.desktop.utils.replaceFirst import java.util.logging.Logger -import javax.inject.Inject -class ContactsViewModel -@Inject -constructor( - private val contactManager: ContactManager, -) { +abstract class ContactsViewModel( + protected val contactManager: ContactManager, + private val conversationManager: ConversationManager, + private val connectionRegistry: ConnectionRegistry +) : EventListener { companion object { private val LOG = Logger.getLogger(ContactsViewModel::class.java.name) } - internal val contacts = mutableStateListOf<Contact>() + private val _fullContactList = mutableListOf<ContactItem>() + private val _filteredContactList = mutableStateListOf<ContactItem>() - internal fun loadContacts() { - val contacts = contactManager.contacts - for (contact in contacts) { - LOG.info("loaded contact: ${contact.author.name} (${contact.alias})") - this.contacts.add(contact) + val contactList: List<ContactItem> = _filteredContactList + + protected open fun filterContact(contact: Contact) = true + + open fun loadContacts() { + _fullContactList.apply { + clear() + addAll( + contactManager.contacts.map { contact -> + ContactItem( + contact, + connectionRegistry.isConnected(contact.id), + conversationManager.getGroupCount(contact.id), + ) + } + ) + } + updateFilteredList() + } + + // todo: when migrated to StateFlow, this could be done implicitly instead + protected open fun updateFilteredList() { + _filteredContactList.apply { + clear() + addAll(_fullContactList.filter { filterContact(it.contact) }) } } - fun getFirst(): Contact? { - return if (contacts.isEmpty()) null else contacts[0] + override fun eventOccurred(e: Event?) { + when (e) { + is ContactAddedEvent -> { + LOG.info("Contact added, reloading") + loadContacts() + } + is ContactConnectedEvent -> { + LOG.info("Contact connected, update state") + updateItem(e.contactId) { it.updateIsConnected(true) } + } + is ContactDisconnectedEvent -> { + LOG.info("Contact disconnected, update state") + updateItem(e.contactId) { it.updateIsConnected(false) } + } + is ContactRemovedEvent -> { + LOG.info("Contact removed, removing item") + removeItem(e.contactId) + } + } + } + + protected open fun updateItem(contactId: ContactId, update: (ContactItem) -> ContactItem) { + _fullContactList.replaceFirst({ it.contact.id == contactId }, update) + updateFilteredList() + } + + protected open fun removeItem(contactId: ContactId) { + _fullContactList.removeFirst { it.contact.id == contactId } + updateFilteredList() } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt index 29542dd7d07d47a172f63a06202775b3e9314a1b..61d13a2f4cd48fa352f4cc2b36cd6909b483624a 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onContactAdd: (Boolean) -> Unit) { +fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onContactAdd: () -> Unit) { TextField( value = searchValue, onValueChange = onValueChange, @@ -36,7 +36,7 @@ fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onCont }, trailingIcon = { IconButton( - onClick = { onContactAdd(true) }, + onClick = onContactAdd, modifier = Modifier.padding(end = 10.dp).size(32.dp) .background(MaterialTheme.colors.primary, CircleShape) ) { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..db2f6dc5062a28c140c4fd422398423a66bdf596 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt @@ -0,0 +1,91 @@ +package org.briarproject.briar.desktop.contact.add.remote + +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.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun AddContactDialog(viewModel: AddContactViewModel, onClose: () -> Unit) { + LaunchedEffect("fetchHandshake") { + // todo: should instead be done automatically as soon as DB is loaded -> in view model + viewModel.fetchHandshakeLink() + } + AlertDialog( + onDismissRequest = onClose, + 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( + viewModel.remoteHandshakeLink.value, + viewModel::setRemoteHandshakeLink, + modifier = Modifier.fillMaxWidth() + ) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + "Contact's Name", + Modifier.width(128.dp).align(Alignment.CenterVertically), + ) + TextField( + viewModel.alias.value, + viewModel::setAddContactAlias, + modifier = Modifier.fillMaxWidth() + ) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + "Your Link", + modifier = Modifier.width(128.dp).align(Alignment.CenterVertically), + ) + TextField( + viewModel.handshakeLink.value, + onValueChange = {}, + modifier = Modifier.fillMaxWidth() + ) + } + } + }, + confirmButton = { + Button(onClick = { viewModel.onSubmitAddContactDialog(); onClose() }) { + Text("Add") + } + }, + dismissButton = { + TextButton( + onClick = onClose + ) { + Text("Cancel", color = MaterialTheme.colors.onSurface) + } + }, + modifier = Modifier.size(600.dp, 300.dp), + ) +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..a0b5d3ca04fac1cbf302f00fe38ad77f1deeecb7 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt @@ -0,0 +1,99 @@ +package org.briarproject.briar.desktop.contact.add.remote + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import org.briarproject.bramble.api.FormatException +import org.briarproject.bramble.api.contact.ContactManager +import org.briarproject.bramble.api.contact.HandshakeLinkConstants +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 java.security.GeneralSecurityException +import java.util.logging.Logger +import javax.inject.Inject + +class AddContactViewModel +@Inject +constructor( + private val contactManager: ContactManager, +) { + + companion object { + private val LOG = Logger.getLogger(AddContactViewModel::class.java.name) + } + + private val _alias = mutableStateOf("") + private val _remoteHandshakeLink = mutableStateOf("") + private val _handshakeLink = mutableStateOf("") + + val alias: State<String> = _alias + val remoteHandshakeLink: State<String> = _remoteHandshakeLink + val handshakeLink: State<String> = _handshakeLink + + fun setAddContactAlias(alias: String) { + _alias.value = alias + } + + fun setRemoteHandshakeLink(link: String) { + _remoteHandshakeLink.value = link + } + + fun fetchHandshakeLink() { + _handshakeLink.value = contactManager.handshakeLink + } + + fun onSubmitAddContactDialog() { + val link = _remoteHandshakeLink.value + val alias = _alias.value + addPendingContact(link, alias) + } + + private fun addPendingContact(link: String, alias: String) { + if (_handshakeLink.value == link) { + println("Please enter contact's link, not your own") + return + } + if (remoteHandshakeLinkIsInvalid(link)) { + println("Remote handshake link is invalid") + return + } + 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 remoteHandshakeLinkIsInvalid(link: String): Boolean { + return !HandshakeLinkConstants.LINK_REGEX.matcher(link).find() + } + + 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/conversation/Conversation.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt index 71f0cd34e0d8295041b8d35a331424c3cd62c2ed..23c61666c1115c531806d12bec299ece7df21be9 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt @@ -18,6 +18,7 @@ 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.ui.Modifier import androidx.compose.ui.graphics.Color @@ -25,6 +26,7 @@ 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.introduction.IntroductionViewModel import org.briarproject.briar.desktop.navigation.SIDEBAR_WIDTH import org.briarproject.briar.desktop.theme.surfaceVariant import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH @@ -32,17 +34,25 @@ import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH @Composable fun Conversation( contact: Contact, - contacts: List<Contact>, - expanded: Boolean, - setExpanded: (Boolean) -> Unit, - infoDrawer: Boolean, - setInfoDrawer: (Boolean) -> Unit, - drawerState: ContactInfoDrawerState + introductionViewModel: IntroductionViewModel, ) { + val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) } + val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) } BoxWithConstraints(Modifier.fillMaxSize()) { val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp) Scaffold( - topBar = { ConversationHeader(contact, expanded, setExpanded, setInfoDrawer) }, + topBar = { + ConversationHeader( + contact, + onMakeIntroduction = { + introductionViewModel.apply { + setFirstContact(contact) + loadContacts() + } + setInfoDrawer(true) + } + ) + }, content = { padding -> Box(modifier = Modifier.padding(padding)) { val chat = ChatState(contact.id) @@ -71,7 +81,7 @@ fun Conversation( RoundedCornerShape(topStart = 10.dp, bottomStart = 10.dp) ) ) { - ContactInfoDrawer(contact, contacts, setInfoDrawer, drawerState) + ContactInfoDrawer(introductionViewModel, setInfoDrawer, contactDrawerState) } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt index c8c45f0e05b09738cef58bafb7f40507901c3c9a..a36dca368671dfc5302184dab43c84e45e50a6a5 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt @@ -14,6 +14,8 @@ 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.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.withTransform @@ -29,10 +31,9 @@ import org.briarproject.briar.desktop.ui.HorizontalDivider @Composable fun ConversationHeader( contact: Contact, - expanded: Boolean, - isExpanded: (Boolean) -> Unit, - setInfoDrawer: (Boolean) -> Unit + onMakeIntroduction: () -> Unit, ) { + val (isExpanded, setExpanded) = remember { mutableStateOf(false) } // TODO hook up online indicator logic val onlineColor = MaterialTheme.colors.secondary val outlineColor = MaterialTheme.colors.outline @@ -57,11 +58,11 @@ fun ConversationHeader( ) } IconButton( - onClick = { isExpanded(!expanded) }, + onClick = { setExpanded(!isExpanded) }, modifier = Modifier.align(Alignment.CenterEnd).padding(end = 16.dp) ) { Icon(Icons.Filled.MoreVert, "contact info", modifier = Modifier.size(24.dp)) - ContactDropDown(expanded, isExpanded, setInfoDrawer) + ContactDropDown(isExpanded, setExpanded, onMakeIntroduction) } HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt index 929c3f076221676d3cdc5bf951174be5a5da055a..b9dc7644181c81c97321295ee10b97a739fd32d4 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt @@ -5,40 +5,30 @@ 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.AddContactDialog -import org.briarproject.briar.desktop.contact.ContactInfoDrawerState.MakeIntro import org.briarproject.briar.desktop.contact.ContactList -import org.briarproject.briar.desktop.contact.ContactsViewModel +import org.briarproject.briar.desktop.contact.ContactListViewModel +import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel +import org.briarproject.briar.desktop.introduction.IntroductionViewModel +import org.briarproject.briar.desktop.ui.UiPlaceholder import org.briarproject.briar.desktop.ui.VerticalDivider @Composable fun PrivateMessageView( - contact: Contact, - contacts: ContactsViewModel, - onContactSelect: (Contact) -> Unit + contactListViewModel: ContactListViewModel, + addContactViewModel: AddContactViewModel, + introductionViewModel: IntroductionViewModel, ) { - 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) + ContactList(contactListViewModel, addContactViewModel) VerticalDivider() Column(modifier = Modifier.weight(1f).fillMaxHeight()) { - Conversation( - contact, - contacts.contacts, - dropdownExpanded, - setExpanded, - infoDrawer, - setInfoDrawer, - contactDrawerState - ) + contactListViewModel.selectedContact.value?.also { selectedContact -> + Conversation( + selectedContact, + introductionViewModel + ) + } ?: UiPlaceholder() } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt similarity index 56% rename from src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt rename to src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt index f8885da16c092b851415d2af2303b01d05c1f36c..b140e1fa65051edec6a51d59b867b5ee15013b86 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt @@ -1,4 +1,4 @@ -package org.briarproject.briar.desktop.contact +package org.briarproject.briar.desktop.introduction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -7,8 +7,8 @@ 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.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Surface @@ -20,44 +20,45 @@ 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.ContactCard +import org.briarproject.briar.desktop.contact.ProfileCircle import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider @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) { +fun ContactDrawerMakeIntro( + viewModel: IntroductionViewModel, + setInfoDrawer: (Boolean) -> Unit +) { + if (!viewModel.secondScreen.value) { 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") + Column { + 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 " + viewModel.firstContact.value!!.author.name + " to:", + fontSize = 16.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) } - 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) + HorizontalDivider() + LazyColumn { + items(viewModel.contactList) { contactItem -> + ContactCard( + contactItem, + { viewModel.setSecondContact(contactItem.contact) }, + false + ) } } } @@ -66,7 +67,7 @@ fun ContactDrawerMakeIntro(contact: Contact, contacts: List<Contact>, setInfoDra Column { Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { IconButton( - onClick = { introNextPg = false }, + onClick = viewModel::backToFirstScreen, 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) @@ -79,26 +80,25 @@ fun ContactDrawerMakeIntro(contact: Contact, contacts: List<Contact>, setInfoDra } 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) + ProfileCircle(36.dp, viewModel.firstContact.value!!.author.id.bytes) + Text(viewModel.firstContact.value!!.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) + ProfileCircle(36.dp, viewModel.secondContact.value!!.author.id.bytes) + Text(viewModel.secondContact.value!!.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 }, + viewModel.introductionMessage.value, + viewModel::setIntroductionMessage, placeholder = { Text(text = "Add a message (optional)") }, ) } Row(Modifier.padding(8.dp)) { TextButton( - onClick = { setInfoDrawer(false); introNextPg = false; }, + onClick = { setInfoDrawer(false) }, Modifier.fillMaxWidth() ) { Text("MAKE INTRODUCTION") diff --git a/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..fda0aa64dc456a881146eeb9992d716b7f2f5a6b --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt @@ -0,0 +1,65 @@ +package org.briarproject.briar.desktop.introduction + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import org.briarproject.bramble.api.connection.ConnectionRegistry +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.bramble.api.contact.ContactManager +import org.briarproject.bramble.api.event.EventBus +import org.briarproject.briar.api.conversation.ConversationManager +import org.briarproject.briar.desktop.contact.ContactsViewModel +import java.util.logging.Logger +import javax.inject.Inject + +class IntroductionViewModel +@Inject +constructor( + contactManager: ContactManager, + conversationManager: ConversationManager, + connectionRegistry: ConnectionRegistry, + eventBus: EventBus, +) : ContactsViewModel(contactManager, conversationManager, connectionRegistry) { + + companion object { + private val LOG = Logger.getLogger(IntroductionViewModel::class.java.name) + } + + init { + // todo: where/when to remove listener again? + eventBus.addListener(this) + } + + private val _firstContact = mutableStateOf<Contact?>(null) + private val _secondContact = mutableStateOf<Contact?>(null) + private val _secondScreen = mutableStateOf(false) + private val _introductionMessage = mutableStateOf("") + + val firstContact: State<Contact?> = _firstContact + val secondContact: State<Contact?> = _secondContact + val secondScreen: State<Boolean> = _secondScreen + val introductionMessage: State<String> = _introductionMessage + + fun setFirstContact(contact: Contact) { + _firstContact.value = contact + loadContacts() + backToFirstScreen() + } + + fun setSecondContact(contact: Contact) { + _secondContact.value = contact + _secondScreen.value = true + } + + fun backToFirstScreen() { + _secondScreen.value = false + _introductionMessage.value = "" + } + + fun setIntroductionMessage(msg: String) { + _introductionMessage.value = msg + } + + override fun filterContact(contact: Contact): Boolean { + return _firstContact.value?.id != contact.id + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index 5fa8ea0d60bf2e40353233dd4e6b62f0a7017026..f621dab9eb5a47577d6700d6010fb7fc320d56d2 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -15,7 +15,9 @@ 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.contact.ContactsViewModel +import org.briarproject.briar.desktop.contact.ContactListViewModel +import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel +import org.briarproject.briar.desktop.introduction.IntroductionViewModel import org.briarproject.briar.desktop.login.Login import org.briarproject.briar.desktop.login.LoginViewModel import org.briarproject.briar.desktop.login.Registration @@ -52,7 +54,9 @@ internal class BriarUiImpl constructor( private val registrationViewModel: RegistrationViewModel, private val loginViewModel: LoginViewModel, - private val contactsViewModel: ContactsViewModel, + private val contactListViewModel: ContactListViewModel, + private val addContactViewModel: AddContactViewModel, + private val introductionViewModel: IntroductionViewModel, private val accountManager: AccountManager, private val contactManager: ContactManager, private val conversationManager: ConversationManager, @@ -82,7 +86,7 @@ constructor( if (accountManager.hasDatabaseKey()) { // this should only happen during testing when we launch the main UI directly // without a need to enter the password. - contactsViewModel.loadContacts() + contactListViewModel.loadContacts() Screen.MAIN } else if (accountManager.accountExists()) { Screen.LOGIN @@ -100,12 +104,11 @@ constructor( when (screenState) { Screen.REGISTRATION -> Registration(registrationViewModel) { - contactsViewModel.loadContacts() screenState = Screen.MAIN } Screen.LOGIN -> Login(loginViewModel) { - contactsViewModel.loadContacts() + contactListViewModel.loadContacts() screenState = Screen.MAIN } else -> @@ -115,7 +118,13 @@ constructor( MM provides messagingManager, IM provides identityManager, ) { - MainScreen(contactsViewModel, isDark, setDark) + MainScreen( + contactListViewModel, + addContactViewModel, + introductionViewModel, + isDark, + setDark + ) } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt index 9a27637bbebd2921e4f4149938f851db1476ecd6..96622e0c932e6318b7dec7a94aa5756cbff42539 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt @@ -1,17 +1,13 @@ package org.briarproject.briar.desktop.ui -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import org.briarproject.briar.desktop.contact.ContactsViewModel +import org.briarproject.briar.desktop.contact.ContactListViewModel +import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel import org.briarproject.briar.desktop.conversation.PrivateMessageView +import org.briarproject.briar.desktop.introduction.IntroductionViewModel import org.briarproject.briar.desktop.navigation.BriarSidebar import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView @@ -22,29 +18,22 @@ import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView */ @Composable fun MainScreen( - contactsViewModel: ContactsViewModel, + contactListViewModel: ContactListViewModel, + addContactViewModel: AddContactViewModel, + introductionViewModel: IntroductionViewModel, isDark: Boolean, setDark: (Boolean) -> Unit ) { // current selected mode, changed using the sidebar buttons 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(contactsViewModel.getFirst()) } // Other global state that we need to track should go here also Row { BriarSidebar(uiMode, setUiMode) VerticalDivider() when (uiMode) { - UiMode.CONTACTS -> if (contact != null) PrivateMessageView( - contact, - contactsViewModel, - setContact - ) + UiMode.CONTACTS -> PrivateMessageView(contactListViewModel, addContactViewModel, introductionViewModel) UiMode.SETTINGS -> PlaceHolderSettingsView(isDark, setDark) - else -> Surface(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)) { - Text("TBD") - } + else -> UiPlaceholder() } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/UiPlaceholder.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiPlaceholder.kt new file mode 100644 index 0000000000000000000000000000000000000000..5b44eea15691334a7dc6b95ac749e889aaa0f730 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiPlaceholder.kt @@ -0,0 +1,14 @@ +package org.briarproject.briar.desktop.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun UiPlaceholder() = Surface(Modifier.fillMaxSize().background(MaterialTheme.colors.background)) { + Text("TBD") +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..3adad7c3f2e0d7746102862e9cefa120c5e2740c --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt @@ -0,0 +1,23 @@ +package org.briarproject.briar.desktop.utils + +fun <T> MutableList<T>.replaceFirst(predicate: (T) -> Boolean, transformation: (T) -> T) { + val li = listIterator() + while (li.hasNext()) { + val n = li.next() + if (predicate(n)) { + li.set(transformation(n)) + break + } + } +} + +fun <T> MutableList<T>.removeFirst(predicate: (T) -> Boolean) { + val li = listIterator() + while (li.hasNext()) { + val n = li.next() + if (predicate(n)) { + li.remove() + break + } + } +}