diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactIdWrapper.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactIdWrapper.kt deleted file mode 100644 index 1521ebe45a9b68913e1d78a9e28f23bba17924a7..0000000000000000000000000000000000000000 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactIdWrapper.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package org.briarproject.briar.desktop.contact - -sealed interface ContactIdWrapper diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt index 0321f6d0616f61e691f3cf7718b0bef6f36b795c..3888c516a8d1d22f94ca8b5b5d4d61737ec6b988 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt @@ -19,15 +19,22 @@ package org.briarproject.briar.desktop.contact import androidx.compose.ui.graphics.ImageBitmap +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.db.Transaction import org.briarproject.bramble.api.identity.AuthorId +import org.briarproject.briar.api.attachment.AttachmentReader import org.briarproject.briar.api.client.MessageTracker +import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.identity.AuthorInfo +import org.briarproject.briar.api.identity.AuthorManager +import org.briarproject.briar.desktop.utils.ImageUtils import org.briarproject.briar.desktop.utils.UiUtils.getContactDisplayName import kotlin.math.max data class ContactItem( - override val idWrapper: RealContactIdWrapper, + val id: ContactId, val authorId: AuthorId, val trustLevel: AuthorInfo.Status, private val name: String, @@ -37,8 +44,11 @@ data class ContactItem( val unread: Int, override val timestamp: Long, val avatar: ImageBitmap?, -) : BaseContactItem { +) : ContactListItem { + data class Id(val id: ContactId) : ContactListItemId + + override val wrapperId = Id(id) override val displayName = getContactDisplayName(name, alias) constructor( @@ -48,7 +58,7 @@ data class ContactItem( groupCount: MessageTracker.GroupCount, avatar: ImageBitmap? ) : this( - idWrapper = RealContactIdWrapper(contact.id), + id = contact.id, authorId = contact.author.id, trustLevel = authorInfo.status, name = contact.author.name, @@ -79,3 +89,21 @@ data class ContactItem( fun updateAvatar(avatar: ImageBitmap?) = copy(avatar = avatar) } + +fun loadContactItem( + txn: Transaction, + contact: Contact, + authorManager: AuthorManager, + connectionRegistry: ConnectionRegistry, + conversationManager: ConversationManager, + attachmentReader: AttachmentReader +): ContactItem { + val authorInfo = authorManager.getAuthorInfo(txn, contact) + return ContactItem( + contact, + authorInfo, + connectionRegistry.isConnected(contact.id), + conversationManager.getGroupCount(txn, contact.id), + authorInfo.avatarHeader?.let { ImageUtils.loadImage(txn, attachmentReader, it) }, + ) +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemView.kt similarity index 56% rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemView.kt index 717f5e1efa21b441812328e238ebb65656842217..bae6112798841f110379930a4083af5186c9f649 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemView.kt @@ -18,7 +18,6 @@ package org.briarproject.briar.desktop.contact -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -28,32 +27,22 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete 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.semantics.Role -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.text import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis import androidx.compose.ui.unit.dp import org.briarproject.bramble.api.contact.ContactId -import org.briarproject.bramble.api.contact.PendingContactId -import org.briarproject.bramble.api.contact.PendingContactState import org.briarproject.bramble.api.identity.AuthorId import org.briarproject.briar.api.identity.AuthorInfo.Status -import org.briarproject.briar.desktop.theme.selectedCard import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE -import org.briarproject.briar.desktop.ui.HorizontalDivider +import org.briarproject.briar.desktop.ui.ListItemView import org.briarproject.briar.desktop.ui.NumberBadge import org.briarproject.briar.desktop.ui.TrustIndicatorLong import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @@ -78,9 +67,9 @@ fun main() = preview( "selected" to false, ) { Column(Modifier.selectableGroup()) { - ContactCard( - ContactItem( - idWrapper = RealContactIdWrapper(ContactId(0)), + ListItemView(getBooleanParameter("selected")) { + val item = ContactItem( + id = ContactId(0), authorId = AuthorId(getRandomIdPersistent()), trustLevel = Status.valueOf(getStringParameter("trustLevel")), name = getStringParameter("name"), @@ -90,68 +79,24 @@ fun main() = preview( unread = getIntParameter("unread"), timestamp = getLongParameter("timestamp"), avatar = null, - ), - {}, getBooleanParameter("selected"), {} - ) - ContactCard( - PendingContactItem( - idWrapper = PendingContactIdWrapper(PendingContactId(getRandomId())), - alias = getStringParameter("alias"), - timestamp = getLongParameter("timestamp"), - state = PendingContactState.ADDING_CONTACT - ), - {}, false, {} - ) - } -} - -@Composable -fun ContactCard( - contactItem: BaseContactItem, - onSel: () -> Unit, - selected: Boolean, - onRemovePending: () -> Unit, -) { - val bgColor = if (selected) MaterialTheme.colors.selectedCard else Color.Transparent - - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = HEADER_SIZE) - .semantics { - contentDescription = if (selected) i18n("access.list.selected.yes") - else i18n("access.list.selected.no") - // todo: stateDescription apparently not used - // stateDescription = if (selected) "selected" else "not selected" - } - .selectable(selected, onClick = onSel, role = Role.Button) - .background(bgColor), - ) { - when (contactItem) { - is ContactItem -> { - RealContactRow(contactItem) - } - - is PendingContactItem -> { - PendingContactRow(contactItem, onRemovePending) - } + ) + ContactItemView(item) } - HorizontalDivider(Modifier.align(Alignment.BottomStart)) } } @Composable -private fun RealContactRow(contactItem: ContactItem) = Row( +fun ContactItemView(contactItem: ContactItem) = Row( horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier + .defaultMinSize(minHeight = HEADER_SIZE) .fillMaxWidth() .padding(vertical = 8.dp) // makes sure that ConnectionIndicator is aligned with AddContact button .padding(start = 16.dp, end = 20.dp) .semantics { - text = getRealContactRowDescription(contactItem) + text = getDescription(contactItem) } ) { Row( @@ -166,7 +111,7 @@ private fun RealContactRow(contactItem: ContactItem) = Row( modifier = Modifier.align(Alignment.TopEnd).offset(6.dp, (-6).dp) ) } - RealContactInfo( + ContactItemViewInfo( contactItem = contactItem, ) } @@ -176,8 +121,9 @@ private fun RealContactRow(contactItem: ContactItem) = Row( ) } -fun getRealContactRowDescription(contactItem: ContactItem) = buildBlankAnnotatedString { +private fun getDescription(contactItem: ContactItem) = buildBlankAnnotatedString { append(i18nF("access.contact.with_name", contactItem.displayName)) + // todo: trust level! appendCommaSeparated( if (contactItem.isConnected) i18n("access.contact.connected.yes") else i18n("access.contact.connected.no") @@ -197,48 +143,7 @@ fun getRealContactRowDescription(contactItem: ContactItem) = buildBlankAnnotated } @Composable -private fun PendingContactRow(contactItem: PendingContactItem, onRemove: () -> Unit) = Row( - horizontalArrangement = spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - // makes sure that Delete button is aligned with AddContact button - .padding(start = 16.dp, end = 4.dp) - .semantics { - text = getPendingContactRowDescription(contactItem) - }, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = spacedBy(12.dp), - modifier = Modifier.weight(1f, fill = true), - ) { - ProfileCircle(36.dp) - PendingContactInfo( - contactItem = contactItem, - ) - } - IconButton( - icon = Icons.Filled.Delete, - contentDescription = i18n("access.contacts.pending.remove"), - onClick = onRemove, - ) -} - -fun getPendingContactRowDescription(contactItem: PendingContactItem) = buildBlankAnnotatedString { - append(i18nF("access.contact.pending.with_name", contactItem.displayName)) - // todo: include pending status - appendCommaSeparated( - i18nF( - "access.contact.pending.added_timestamp", - getFormattedTimestamp(contactItem.timestamp) - ) - ) -} - -@Composable -private fun RealContactInfo(contactItem: ContactItem) = Column( +private fun ContactItemViewInfo(contactItem: ContactItem) = Column( horizontalAlignment = Alignment.Start, verticalArrangement = spacedBy(2.dp), ) { @@ -257,20 +162,3 @@ private fun RealContactInfo(contactItem: ContactItem) = Column( style = MaterialTheme.typography.caption, ) } - -@Composable -private fun PendingContactInfo(contactItem: PendingContactItem) = Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = spacedBy(2.dp), -) { - Text( - text = contactItem.displayName, - style = MaterialTheme.typography.body1, - maxLines = 3, - overflow = Ellipsis, - ) - Text( - text = getFormattedTimestamp(contactItem.timestamp), - style = MaterialTheme.typography.caption, - ) -} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemViewSmall.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemViewSmall.kt new file mode 100644 index 0000000000000000000000000000000000000000..fda601dc0d796f32c5d2c00c219bd6b394382434 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemViewSmall.kt @@ -0,0 +1,131 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.contact + +import androidx.compose.foundation.layout.Arrangement.spacedBy +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.requiredSize +import androidx.compose.foundation.selection.selectableGroup +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.semantics.semantics +import androidx.compose.ui.semantics.text +import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis +import androidx.compose.ui.unit.dp +import org.briarproject.bramble.api.contact.ContactId +import org.briarproject.bramble.api.identity.AuthorId +import org.briarproject.briar.api.identity.AuthorInfo.Status +import org.briarproject.briar.desktop.ui.ListItemView +import org.briarproject.briar.desktop.ui.TrustIndicatorShort +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF +import org.briarproject.briar.desktop.utils.PreviewUtils.DropDownValues +import org.briarproject.briar.desktop.utils.PreviewUtils.preview +import org.briarproject.briar.desktop.utils.appendCommaSeparated +import org.briarproject.briar.desktop.utils.buildBlankAnnotatedString +import java.time.Instant + +@Suppress("HardCodedStringLiteral") +fun main() = preview( + "name" to "Paul", + "alias" to "UI Master", + "trustLevel" to DropDownValues(0, Status.values().filterNot { it == Status.NONE }.map { it.name }), + "isConnected" to true, + "isEmpty" to false, + "unread" to 3, + "timestamp" to Instant.now().toEpochMilli(), + "selected" to false, +) { + Column(Modifier.selectableGroup()) { + ListItemView(getBooleanParameter("selected")) { + ContactItemViewSmall( + ContactItem( + id = ContactId(0), + authorId = AuthorId(getRandomIdPersistent()), + trustLevel = Status.valueOf(getStringParameter("trustLevel")), + name = getStringParameter("name"), + alias = getStringParameter("alias"), + isConnected = getBooleanParameter("isConnected"), + isEmpty = getBooleanParameter("isEmpty"), + unread = getIntParameter("unread"), + timestamp = getLongParameter("timestamp"), + avatar = null, + ), + ) + } + } +} + +@Composable +fun ContactItemViewSmall( + contactItem: ContactItem, + showConnectionState: Boolean = true, +) = Row( + horizontalArrangement = spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + // makes sure that ConnectionIndicator is aligned with AddContact button + .padding(start = 16.dp, end = 20.dp) + .semantics { + text = getDescription(contactItem, showConnectionState) + } +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + modifier = Modifier.weight(1f, fill = true), + ) { + // TODO cache profile images, if available + ProfileCircle(20.dp, contactItem) + Text( + text = contactItem.displayName, + style = MaterialTheme.typography.body1, + maxLines = 3, + overflow = Ellipsis, + ) + TrustIndicatorShort(contactItem.trustLevel) + } + if (showConnectionState) + ConnectionIndicator( + modifier = Modifier.requiredSize(12.dp), + isConnected = contactItem.isConnected + ) +} + +private fun getDescription( + contactItem: ContactItem, + showConnectionState: Boolean, +) = buildBlankAnnotatedString { + append(i18nF("access.contact.with_name", contactItem.displayName)) + // todo: trust level! + if (showConnectionState) + appendCommaSeparated( + if (contactItem.isConnected) i18n("access.contact.connected.yes") + else i18n("access.contact.connected.no") + ) + append('.') +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt index c2b33acd9b8ce8e2a57f545cc017611b01b7ffb4..61ecfb57f6234659881a1d8cfce989035ec14dfb 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt @@ -36,22 +36,91 @@ import androidx.compose.material.Surface import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PersonAdd 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.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import org.briarproject.bramble.api.contact.ContactId +import org.briarproject.bramble.api.contact.PendingContactId +import org.briarproject.bramble.api.contact.PendingContactState +import org.briarproject.bramble.api.identity.AuthorId +import org.briarproject.briar.api.identity.AuthorInfo +import org.briarproject.briar.desktop.contact.add.remote.PendingContactItem +import org.briarproject.briar.desktop.contact.add.remote.PendingContactItemView import org.briarproject.briar.desktop.theme.surfaceVariant import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE +import org.briarproject.briar.desktop.ui.ListItemView import org.briarproject.briar.desktop.ui.SearchTextField import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.utils.PreviewUtils.preview +import java.time.Instant + +@Suppress("HardCodedStringLiteral") +fun main() = preview { + val list = remember { + listOf( + ContactItem( + id = ContactId(0), + authorId = AuthorId(getRandomId()), + trustLevel = AuthorInfo.Status.VERIFIED, + name = "Maria", + alias = "Mary", + isConnected = true, + isEmpty = false, + unread = 2, + timestamp = Instant.now().toEpochMilli(), + avatar = null, + ), + PendingContactItem( + id = PendingContactId(getRandomId()), + alias = "Thomas", + timestamp = (Instant.now().minusSeconds(300)).toEpochMilli(), + state = PendingContactState.ADDING_CONTACT, + ), + ContactItem( + id = ContactId(1), + authorId = AuthorId(getRandomId()), + trustLevel = AuthorInfo.Status.UNVERIFIED, + name = "Anna", + alias = null, + isConnected = false, + isEmpty = true, + unread = 0, + timestamp = (Instant.now().minusSeconds(300)).toEpochMilli(), + avatar = null, + ), + ) + } + + val (selected, setSelected) = remember { mutableStateOf<ContactListItem?>(null) } + val (filterBy, setFilterBy) = remember { mutableStateOf("") } + + val filteredList = remember(filterBy) { + list.filter { + it.displayName.contains(filterBy, ignoreCase = true) + }.sortedByDescending { it.timestamp } + } + + ContactList( + contactList = filteredList, + isSelected = { selected == it }, + selectContact = setSelected, + removePendingContact = {}, + filterBy = filterBy, + setFilterBy = setFilterBy, + onContactAdd = {}, + ) +} @Composable fun ContactList( - contactList: List<BaseContactItem>, - isSelected: (BaseContactItem) -> Boolean, - selectContact: (BaseContactItem) -> Unit, + contactList: List<ContactListItem>, + isSelected: (ContactListItem) -> Boolean, + selectContact: (ContactListItem) -> Unit, removePendingContact: (PendingContactItem) -> Unit, filterBy: String, setFilterBy: (String) -> Unit, @@ -86,17 +155,30 @@ fun ContactList( } .selectableGroup() ) { - items(contactList) { contactItem -> - ContactCard( - contactItem, - onSel = { selectContact(contactItem) }, - selected = isSelected(contactItem), - onRemovePending = { - if (contactItem is PendingContactItem) { - removePendingContact(contactItem) + items( + items = contactList, + key = { item -> item.wrapperId }, + contentType = { item -> item::class } + ) { item -> + ListItemView( + onSelect = { selectContact(item) }, + selected = isSelected(item), + ) { + when (item) { + is ContactItem -> { + ContactItemView(item) } - }, - ) + + is PendingContactItem -> { + PendingContactItemView( + contactItem = item, + onRemove = { + removePendingContact(item) + }, + ) + } + } + } } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/BaseContactItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListItem.kt similarity index 90% rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/BaseContactItem.kt rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListItem.kt index edc3c7cef678d062bfd2271ceed5cae3ca4bdf41..76a3a924115239f4f99224bb868a7f57bf28af10 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/BaseContactItem.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListItem.kt @@ -18,9 +18,10 @@ package org.briarproject.briar.desktop.contact -sealed interface BaseContactItem { - - val idWrapper: ContactIdWrapper +interface ContactListItem { val displayName: String val timestamp: Long + val wrapperId: ContactListItemId } + +interface ContactListItemId diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt index e242cbccfbdbd9c7dc088f2d364fca1769c04239..78a1934d843c807825e2acd350bee9a7e787f445 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt @@ -19,26 +19,29 @@ package org.briarproject.briar.desktop.contact import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import mu.KotlinLogging import org.briarproject.bramble.api.connection.ConnectionRegistry import org.briarproject.bramble.api.contact.ContactManager import org.briarproject.bramble.api.contact.PendingContactId import org.briarproject.bramble.api.contact.PendingContactState -import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent +import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent +import org.briarproject.bramble.api.db.Transaction import org.briarproject.bramble.api.db.TransactionManager import org.briarproject.bramble.api.event.Event import org.briarproject.bramble.api.event.EventBus import org.briarproject.bramble.api.lifecycle.LifecycleManager import org.briarproject.briar.api.attachment.AttachmentReader -import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.conversation.event.ConversationMessageTrackedEvent import org.briarproject.briar.api.identity.AuthorManager +import org.briarproject.briar.desktop.contact.add.remote.PendingContactItem import org.briarproject.briar.desktop.conversation.ConversationMessagesReadEvent import org.briarproject.briar.desktop.threading.BriarExecutors -import org.briarproject.briar.desktop.utils.ImageUtils import org.briarproject.briar.desktop.utils.KLoggerUtils.i +import org.briarproject.briar.desktop.utils.clearAndAddAll +import org.briarproject.briar.desktop.utils.removeFirst import org.briarproject.briar.desktop.viewmodel.asState import javax.inject.Inject @@ -49,7 +52,7 @@ constructor( authorManager: AuthorManager, conversationManager: ConversationManager, connectionRegistry: ConnectionRegistry, - private val attachmentReader: AttachmentReader, + attachmentReader: AttachmentReader, briarExecutors: BriarExecutors, lifecycleManager: LifecycleManager, db: TransactionManager, @@ -75,16 +78,30 @@ constructor( loadContacts() } + private val _pendingContactList = mutableStateListOf<PendingContactItem>() + private val _filterBy = mutableStateOf("") - private val _selectedContactId = mutableStateOf<ContactIdWrapper?>(null) + private val _selectedContactId = mutableStateOf<ContactListItemId?>(null) private val _contactIdToBeRemoved = mutableStateOf<PendingContactId?>(null) + // todo: check impact on performance due to reconstructing whole list on every change + val combinedContactList = derivedStateOf { + (_contactList + _pendingContactList) + .filter { + it.displayName.contains(_filterBy.value, ignoreCase = true) + }.sortedByDescending { it.timestamp } + } + + val noContactsYet = derivedStateOf { + _contactList.isEmpty() && _pendingContactList.isEmpty() + } + val filterBy = _filterBy.asState() val selectedContactId = derivedStateOf { // reset selected contact to null if not part of list after filtering - val id = _selectedContactId.value - if (id == null || contactList.value.map { it.idWrapper }.contains(id)) { - id + val wrapperId = _selectedContactId.value + if (wrapperId == null || combinedContactList.value.find { it.wrapperId == wrapperId } != null) { + wrapperId } else { _selectedContactId.value = null null @@ -92,19 +109,32 @@ constructor( } val removePendingContactDialogVisible = derivedStateOf { _contactIdToBeRemoved.value != null } - fun selectContact(contactItem: BaseContactItem) { - _selectedContactId.value = contactItem.idWrapper + override fun loadContactsWithinTransaction(txn: Transaction) { + // load real contacts + super.loadContactsWithinTransaction(txn) + + // load pending contacts + val pendingContactList = contactManager.getPendingContacts(txn).map { contact -> + PendingContactItem(contact.first, contact.second) + } + txn.attach { + _pendingContactList.clearAndAddAll(pendingContactList) + } + } + + fun selectContact(contactItem: ContactListItem) { + _selectedContactId.value = contactItem.wrapperId } - fun isSelected(contactItem: BaseContactItem) = _selectedContactId.value == contactItem.idWrapper + fun isSelected(contactItem: ContactListItem) = _selectedContactId.value == contactItem.wrapperId fun removePendingContact(contactItem: PendingContactItem) { if (contactItem.state == PendingContactState.FAILED) { // no need to show warning dialog for failed pending contacts - removePendingContact(contactItem.idWrapper.contactId) + removePendingContact(contactItem.id) } else { // show warning dialog - _contactIdToBeRemoved.value = contactItem.idWrapper.contactId + _contactIdToBeRemoved.value = contactItem.id } } @@ -123,14 +153,11 @@ constructor( contactManager.removePendingContact(txn, contactId) _contactIdToBeRemoved.value = null txn.attach { - removeItem(contactId) + removePendingContactItem(contactId) } } } - override fun filterContactItem(contactItem: BaseContactItem) = - contactItem.displayName.contains(_filterBy.value, ignoreCase = true) - fun setFilterBy(filter: String) { _filterBy.value = filter } @@ -140,28 +167,24 @@ constructor( when (e) { is ConversationMessageTrackedEvent -> { LOG.i { "Conversation message tracked, updating item" } - updateItem(e.contactId) { it.updateTimestampAndUnread(e.timestamp, e.read) } - } - is ContactAliasChangedEvent -> { - updateItem(e.contactId) { it.updateAlias(e.alias) } + updateContactItem(e.contactId) { it.updateTimestampAndUnread(e.timestamp, e.read) } } + is ConversationMessagesReadEvent -> { LOG.i { "${e.count} conversation messages read, updating item" } - updateItem(e.contactId) { it.updateFromMessagesRead(e.count) } + updateContactItem(e.contactId) { it.updateFromMessagesRead(e.count) } } - is AvatarUpdatedEvent -> { - LOG.i { "received avatar update: ${e.attachmentHeader}" } - if (e.attachmentHeader == null) { - updateItem(e.contactId) { it.updateAvatar(null) } - } else { - runOnDbThreadWithTransaction(true) { txn -> - val image = ImageUtils.loadImage(txn, attachmentReader, e.attachmentHeader) - txn.attach { - updateItem(e.contactId) { it.updateAvatar(image) } - } - } - } + + is PendingContactAddedEvent -> { + LOG.i { "Pending contact added, reloading" } + loadContacts() } + + // todo: is PendingContactRemovedEvent + // todo: is PendingContactStateChangedEvent } } + + private fun removePendingContactItem(contactId: PendingContactId) = + _pendingContactList.removeFirst { it.id == contactId } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt index f8af78fb4c3988bbbc446fbc572275e31b2eb242..0cc29e6278f96beb12ea23ad1c3bd0b4d4c05882 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt @@ -18,16 +18,16 @@ package org.briarproject.briar.desktop.contact -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateListOf import mu.KotlinLogging 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.PendingContactId import org.briarproject.bramble.api.contact.event.ContactAddedEvent +import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent import org.briarproject.bramble.api.contact.event.ContactRemovedEvent -import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent +import org.briarproject.bramble.api.db.Transaction import org.briarproject.bramble.api.db.TransactionManager import org.briarproject.bramble.api.event.Event import org.briarproject.bramble.api.event.EventBus @@ -35,9 +35,11 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent import org.briarproject.briar.api.attachment.AttachmentReader +import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.identity.AuthorManager import org.briarproject.briar.desktop.threading.BriarExecutors +import org.briarproject.briar.desktop.threading.UiExecutor import org.briarproject.briar.desktop.utils.ImageUtils import org.briarproject.briar.desktop.utils.KLoggerUtils.i import org.briarproject.briar.desktop.utils.clearAndAddAll @@ -61,87 +63,66 @@ abstract class ContactsViewModel( private val LOG = KotlinLogging.logger {} } - private val _fullContactList = mutableStateListOf<BaseContactItem>() + protected val _contactList = mutableStateListOf<ContactItem>() - val noContactsYet = derivedStateOf { - _fullContactList.isEmpty() - } - - val contactList = derivedStateOf { - _fullContactList.filter(::filterContactItem).sortedByDescending { it.timestamp } - } + @UiExecutor + fun loadContacts() = runOnDbThreadWithTransaction(true, ::loadContactsWithinTransaction) - protected open fun filterContactItem(contactItem: BaseContactItem) = true + open fun loadContactsWithinTransaction(txn: Transaction) { + val contactList = contactManager.getContacts(txn).map { contact -> + loadContactItemWithinTransaction(txn, contact) + } - open fun loadContacts() { - val contactList = mutableListOf<BaseContactItem>() - runOnDbThreadWithTransaction(true) { txn -> - contactList.addAll( - contactManager.getPendingContacts(txn).map { contact -> - PendingContactItem(contact.first, contact.second) - } - ) - contactList.addAll( - contactManager.getContacts(txn).map { contact -> - val authorInfo = authorManager.getAuthorInfo(txn, contact) - ContactItem( - contact, - authorInfo, - connectionRegistry.isConnected(contact.id), - conversationManager.getGroupCount(txn, contact.id), - authorInfo.avatarHeader?.let { ImageUtils.loadImage(txn, attachmentReader, it) }, - ) - } - ) - txn.attach { - _fullContactList.clearAndAddAll(contactList) - } + txn.attach { + _contactList.clearAndAddAll(contactList) } } + open fun loadContactItemWithinTransaction(txn: Transaction, contact: Contact) = + loadContactItem(txn, contact, authorManager, connectionRegistry, conversationManager, attachmentReader) + override fun eventOccurred(e: Event?) { when (e) { is ContactAddedEvent -> { + // todo: instead, add single new item! LOG.i { "Contact added, reloading" } loadContacts() } - is PendingContactAddedEvent -> { - LOG.i { "Pending contact added, reloading" } - loadContacts() - } + is ContactConnectedEvent -> { LOG.i { "Contact connected, update state" } - updateItem(e.contactId) { - it.updateIsConnected(true) - } + updateContactItem(e.contactId) { it.updateIsConnected(true) } } + is ContactDisconnectedEvent -> { LOG.i { "Contact disconnected, update state" } - updateItem(e.contactId) { it.updateIsConnected(false) } + updateContactItem(e.contactId) { it.updateIsConnected(false) } } + is ContactRemovedEvent -> { LOG.i { "Contact removed, removing item" } - removeItem(e.contactId) + removeContactItem(e.contactId) } - } - } - protected open fun updateItem(contactId: ContactId, update: (ContactItem) -> ContactItem) { - _fullContactList.replaceFirst( - { it.idWrapper.contactId == contactId }, - update - ) - } + is ContactAliasChangedEvent -> { + updateContactItem(e.contactId) { it.updateAlias(e.alias) } + } - protected open fun removeItem(contactId: ContactId) { - _fullContactList.removeFirst<BaseContactItem, ContactItem> { - it.idWrapper.contactId == contactId + is AvatarUpdatedEvent -> { + LOG.i { "received avatar update: ${e.attachmentHeader}" } + runOnDbThreadWithTransaction(true) { txn -> + val image = ImageUtils.loadImage(txn, attachmentReader, e.attachmentHeader) + txn.attach { + updateContactItem(e.contactId) { it.updateAvatar(image) } + } + } + } } } - protected open fun removeItem(contactId: PendingContactId) { - _fullContactList.removeFirst<BaseContactItem, PendingContactItem> { - it.idWrapper.contactId == contactId - } - } + protected fun updateContactItem(contactId: ContactId, update: (ContactItem) -> ContactItem) = + _contactList.replaceFirst({ it.id == contactId }, update) + + protected fun removeContactItem(contactId: ContactId) = + _contactList.removeFirst { it.id == contactId } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/PendingContactIdWrapper.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/PendingContactIdWrapper.kt deleted file mode 100644 index be8746e621db7ee025ac76106e9b25aadbc5cc98..0000000000000000000000000000000000000000 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/PendingContactIdWrapper.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package org.briarproject.briar.desktop.contact - -import org.briarproject.bramble.api.contact.PendingContactId - -data class PendingContactIdWrapper(val contactId: PendingContactId) : ContactIdWrapper diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/RealContactIdWrapper.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/RealContactIdWrapper.kt deleted file mode 100644 index bb2a07c10c2ab3ba3c53f36a9b52013ac7ccb722..0000000000000000000000000000000000000000 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/RealContactIdWrapper.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package org.briarproject.briar.desktop.contact - -import org.briarproject.bramble.api.contact.ContactId - -data class RealContactIdWrapper(val contactId: ContactId) : ContactIdWrapper diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/PendingContactItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/PendingContactItem.kt similarity index 74% rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/PendingContactItem.kt rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/PendingContactItem.kt index 59e5f4a3eb693e34efbf87fe2d76f928d8f1c07e..5f7965c6eed87410477c01a060e178b11c30b742 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/PendingContactItem.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/PendingContactItem.kt @@ -16,22 +16,27 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -package org.briarproject.briar.desktop.contact +package org.briarproject.briar.desktop.contact.add.remote import org.briarproject.bramble.api.contact.PendingContact +import org.briarproject.bramble.api.contact.PendingContactId import org.briarproject.bramble.api.contact.PendingContactState +import org.briarproject.briar.desktop.contact.ContactListItem +import org.briarproject.briar.desktop.contact.ContactListItemId data class PendingContactItem( - override val idWrapper: PendingContactIdWrapper, + val id: PendingContactId, val alias: String, override val timestamp: Long, val state: PendingContactState, -) : BaseContactItem { +) : ContactListItem { + data class Id(val id: PendingContactId) : ContactListItemId + override val wrapperId = Id(id) override val displayName = alias constructor(contact: PendingContact, state: PendingContactState) : this( - idWrapper = PendingContactIdWrapper(contact.id), + id = contact.id, alias = contact.alias, timestamp = contact.timestamp, state = state, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/PendingContactItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/PendingContactItemView.kt new file mode 100644 index 0000000000000000000000000000000000000000..357c887b0003be05c82101b11d5835a10f3a9381 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/PendingContactItemView.kt @@ -0,0 +1,136 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.contact.add.remote + +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectableGroup +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.Delete +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.text +import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis +import androidx.compose.ui.unit.dp +import org.briarproject.bramble.api.contact.PendingContactId +import org.briarproject.bramble.api.contact.PendingContactState +import org.briarproject.briar.api.identity.AuthorInfo.Status +import org.briarproject.briar.desktop.contact.ProfileCircle +import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE +import org.briarproject.briar.desktop.ui.ListItemView +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF +import org.briarproject.briar.desktop.utils.PreviewUtils.DropDownValues +import org.briarproject.briar.desktop.utils.PreviewUtils.preview +import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp +import org.briarproject.briar.desktop.utils.appendCommaSeparated +import org.briarproject.briar.desktop.utils.buildBlankAnnotatedString +import java.time.Instant + +@Suppress("HardCodedStringLiteral") +fun main() = preview( + "name" to "Paul", + "alias" to "UI Master", + "trustLevel" to DropDownValues(0, Status.values().filterNot { it == Status.NONE }.map { it.name }), + "isConnected" to true, + "isEmpty" to false, + "unread" to 3, + "timestamp" to Instant.now().toEpochMilli(), + "selected" to false, +) { + Column(Modifier.selectableGroup()) { + ListItemView(getBooleanParameter("selected")) { + val item = PendingContactItem( + id = PendingContactId(getRandomId()), + alias = getStringParameter("alias"), + timestamp = getLongParameter("timestamp"), + state = PendingContactState.ADDING_CONTACT + ) + PendingContactItemView(item, onRemove = {}) + } + } +} + +@Composable +fun PendingContactItemView(contactItem: PendingContactItem, onRemove: () -> Unit) = Row( + horizontalArrangement = spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .defaultMinSize(minHeight = HEADER_SIZE) + .fillMaxWidth() + .padding(vertical = 8.dp) + // makes sure that Delete button is aligned with AddContact button + .padding(start = 16.dp, end = 4.dp) + .semantics { + text = getDescription(contactItem) + }, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(12.dp), + modifier = Modifier.weight(1f, fill = true), + ) { + ProfileCircle(36.dp) + PendingContactItemViewInfo( + contactItem = contactItem, + ) + } + IconButton( + icon = Icons.Filled.Delete, + contentDescription = i18n("access.contacts.pending.remove"), + onClick = onRemove, + ) +} + +private fun getDescription(contactItem: PendingContactItem) = buildBlankAnnotatedString { + append(i18nF("access.contact.pending.with_name", contactItem.alias)) + // todo: include pending status + appendCommaSeparated( + i18nF( + "access.contact.pending.added_timestamp", + getFormattedTimestamp(contactItem.timestamp) + ) + ) +} + +@Composable +private fun PendingContactItemViewInfo(contactItem: PendingContactItem) = Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = spacedBy(2.dp), +) { + Text( + text = contactItem.alias, + style = MaterialTheme.typography.body1, + maxLines = 3, + overflow = Ellipsis, + ) + Text( + text = getFormattedTimestamp(contactItem.timestamp), + style = MaterialTheme.typography.caption, + ) +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt index 6d11586d81a22a182ebf231ed6c62a4030a482cd..fee8ff4c99c06eb981323d3186b6211130c555d4 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt @@ -60,10 +60,10 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader import org.briarproject.briar.desktop.DesktopFeatureFlags import org.briarproject.briar.desktop.attachment.media.ImageCompressor import org.briarproject.briar.desktop.contact.ContactItem +import org.briarproject.briar.desktop.contact.loadContactItem import org.briarproject.briar.desktop.conversation.ConversationRequestItem.RequestType.FORUM import org.briarproject.briar.desktop.conversation.ConversationRequestItem.RequestType.INTRODUCTION import org.briarproject.briar.desktop.threading.BriarExecutors -import org.briarproject.briar.desktop.utils.ImageUtils.loadImage import org.briarproject.briar.desktop.utils.KLoggerUtils.e import org.briarproject.briar.desktop.utils.KLoggerUtils.i import org.briarproject.briar.desktop.utils.KLoggerUtils.logDuration @@ -285,14 +285,8 @@ constructor( val start = LogUtils.now() val contact = contactManager.getContact(txn, id) - val authorInfo = authorManager.getAuthorInfo(txn, contact) - val contactItem = ContactItem( - contact, - authorInfo, - connectionRegistry.isConnected(id), - conversationManager.getGroupCount(txn, id), - authorInfo.avatarHeader?.let { loadImage(txn, attachmentReader, it) }, - ) + val contactItem = + loadContactItem(txn, contact, authorManager, connectionRegistry, conversationManager, attachmentReader) LOG.logDuration("Loading contact", start) txn.attach { _contactItem.value = contactItem @@ -314,7 +308,7 @@ constructor( _loadingMessages.value = true try { var start = LogUtils.now() - val headers = conversationManager.getMessageHeaders(txn, contact.idWrapper.contactId) + val headers = conversationManager.getMessageHeaders(txn, contact.id) LOG.logDuration("Loading message headers", start) // Sort headers by timestamp in *ascending* order val sorted = headers.sortedBy { it.timestamp } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt index 42065c51fbebd05694032f75d8096e7f85b9de06..5f4b2bd7984d9307fdfd702be7129c4ffdc736fc 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt @@ -37,12 +37,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.contact.ConfirmRemovePendingContactDialog +import org.briarproject.briar.desktop.contact.ContactItem import org.briarproject.briar.desktop.contact.ContactList import org.briarproject.briar.desktop.contact.ContactListViewModel -import org.briarproject.briar.desktop.contact.PendingContactIdWrapper -import org.briarproject.briar.desktop.contact.RealContactIdWrapper import org.briarproject.briar.desktop.contact.add.remote.AddContactDialog import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel +import org.briarproject.briar.desktop.contact.add.remote.PendingContactItem import org.briarproject.briar.desktop.ui.BriarLogo import org.briarproject.briar.desktop.ui.ColoredIconButton import org.briarproject.briar.desktop.ui.Constants.PARAGRAPH_WIDTH @@ -70,7 +70,7 @@ fun PrivateMessageScreen( Row(modifier = Modifier.fillMaxWidth()) { ContactList( - viewModel.contactList.value, + viewModel.combinedContactList.value, viewModel::isSelected, viewModel::selectContact, viewModel::removePendingContact, @@ -80,14 +80,14 @@ fun PrivateMessageScreen( ) VerticalDivider() Column(modifier = Modifier.weight(1f).fillMaxHeight()) { - val id = viewModel.selectedContactId.value - if (id == null) { + val wrapperId = viewModel.selectedContactId.value + if (wrapperId == null) { NoContactSelected() - } else when (id) { - is RealContactIdWrapper -> { - ConversationScreen(id.contactId) + } else when (wrapperId) { + is ContactItem.Id -> { + ConversationScreen(wrapperId.id) } - is PendingContactIdWrapper -> { + is PendingContactItem.Id -> { PendingContactSelected() } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt index 14ea3742af0a748ea43145049802c15b80d8614d..3bfa7535a28e419000afaa8cf083c5f682b65b78 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt @@ -42,11 +42,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.briarproject.bramble.api.sync.GroupId -import org.briarproject.briar.desktop.contact.ContactCard +import org.briarproject.briar.desktop.contact.ContactItemViewSmall import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider +import org.briarproject.briar.desktop.ui.ListItemView import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n -import org.briarproject.briar.desktop.viewmodel.viewModel @Composable fun ForumSharingDrawerContent( @@ -97,12 +97,9 @@ fun ForumSharingDrawerContent( } else { LazyColumn { items(viewModel.currentlySharedWith) { contactItem -> - ContactCard( - contactItem, - onSel = {}, - selected = false, - onRemovePending = {}, - ) + ListItemView { + ContactItemViewSmall(contactItem) + } } } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt index 3ddff4f582030405bb029102e83e41c2b893f428..7e6ac6a5a97ffb10db2119fa5817f4eda47e4362 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt @@ -103,20 +103,20 @@ class ForumSharingViewModel @Inject constructor( } e is ContactLeftShareableEvent && e.groupId == groupId -> { - _currentlySharedWith.removeFirst { it.idWrapper.contactId == e.contactId } + _currentlySharedWith.removeFirst { it.id == e.contactId } val connected = connectionRegistry.isConnected(e.contactId) _sharingInfo.update { removeContact(connected) } } e is ContactConnectedEvent -> { - val isMember = _currentlySharedWith.replaceFirst({ it.idWrapper.contactId == e.contactId }) { + val isMember = _currentlySharedWith.replaceFirst({ it.id == e.contactId }) { it.updateIsConnected(true) } if (isMember) _sharingInfo.update { updateContactConnected(true) } } e is ContactDisconnectedEvent -> { - val isMember = _currentlySharedWith.replaceFirst({ it.idWrapper.contactId == e.contactId }) { + val isMember = _currentlySharedWith.replaceFirst({ it.id == e.contactId }) { it.updateIsConnected(false) } if (isMember) _sharingInfo.update { updateContactConnected(false) } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt index de17bd810dce5f79345f5b1ee9c5795eb82fe79b..fc835a87e711ec6f04281cd51a61e7991139fa9b 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt @@ -44,12 +44,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import org.briarproject.briar.desktop.contact.ContactCard import org.briarproject.briar.desktop.contact.ContactItem +import org.briarproject.briar.desktop.contact.ContactItemView import org.briarproject.briar.desktop.contact.ProfileCircle import org.briarproject.briar.desktop.theme.surfaceVariant import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider +import org.briarproject.briar.desktop.ui.ListItemView import org.briarproject.briar.desktop.utils.InternationalizationUtils import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF @@ -83,13 +84,12 @@ fun ContactDrawerMakeIntro( HorizontalDivider() LazyColumn { items(viewModel.contactList.value) { contactItem -> - if (contactItem is ContactItem) - ContactCard( - contactItem, - onSel = { viewModel.setSecondContact(contactItem) }, - selected = false, - onRemovePending = {}, - ) + ListItemView( + selected = false, + onSelect = { viewModel.setSecondContact(contactItem) }, + ) { + ContactItemView(contactItem) + } } } } else { diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt index 961b3b3dd4378502c185b16628977d0d92ba1ee2..361ec9f67357c94a74cfa1e7e38f4e2ef378a1b6 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt @@ -18,6 +18,7 @@ package org.briarproject.briar.desktop.introduction +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import org.briarproject.bramble.api.connection.ConnectionRegistry import org.briarproject.bramble.api.contact.ContactManager @@ -28,7 +29,6 @@ import org.briarproject.briar.api.attachment.AttachmentReader import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.identity.AuthorManager import org.briarproject.briar.api.introduction.IntroductionManager -import org.briarproject.briar.desktop.contact.BaseContactItem import org.briarproject.briar.desktop.contact.ContactItem import org.briarproject.briar.desktop.contact.ContactsViewModel import org.briarproject.briar.desktop.threading.BriarExecutors @@ -70,6 +70,12 @@ constructor( val secondScreen = _secondScreen.asState() val introductionMessage = _introductionMessage.asState() + val contactList = derivedStateOf { + _contactList.filter { + it.id != _firstContact.value?.id + }.sortedByDescending { it.displayName } + } + fun setFirstContact(contactItem: ContactItem) { _firstContact.value = contactItem loadContacts() @@ -90,12 +96,6 @@ constructor( _introductionMessage.value = msg } - override fun filterContactItem(contactItem: BaseContactItem): Boolean { - return if (contactItem is ContactItem) { - _firstContact.value?.idWrapper != contactItem.idWrapper - } else false - } - fun makeIntroduction() { val c1 = requireNotNull(_firstContact.value) val c2 = requireNotNull(_secondContact.value) @@ -104,8 +104,8 @@ constructor( val msg = _introductionMessage.value.ifEmpty { null } runOnDbThread { - val c1 = contactManager.getContact(c1.idWrapper.contactId) - val c2 = contactManager.getContact(c2.idWrapper.contactId) + val c1 = contactManager.getContact(c1.id) + val c2 = contactManager.getContact(c2.id) introductionManager.makeIntroduction(c1, c2, msg) } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/ListItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/ListItemView.kt new file mode 100644 index 0000000000000000000000000000000000000000..b8dc6cd8bc03bc0752caf048ed432174b2c7acff --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/ListItemView.kt @@ -0,0 +1,66 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.MaterialTheme +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.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import org.briarproject.briar.desktop.theme.selectedCard +import org.briarproject.briar.desktop.utils.InternationalizationUtils + +@Composable +fun ListItemView( + selected: Boolean? = null, + onSelect: () -> Unit = {}, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val bgColor = if (selected != null && selected) MaterialTheme.colors.selectedCard else Color.Transparent + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .fillMaxWidth() + .background(bgColor) + .then( + if (selected != null) { + Modifier + .semantics { + contentDescription = if (selected) InternationalizationUtils.i18n("access.list.selected.yes") + else InternationalizationUtils.i18n("access.list.selected.no") + // todo: stateDescription apparently not used + // stateDescription = if (selected) "selected" else "not selected" + } + .selectable(selected, onClick = onSelect, role = Role.Button) + } else Modifier + ) + ) { + content() + HorizontalDivider(Modifier.align(Alignment.BottomStart)) + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/TrustIndicator.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/TrustIndicator.kt index 73bb089b58bcd3bb90c45fbe20c8121e9f2101ce..f47480991a2bf2d788d3f467c78df38b6f912795 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/TrustIndicator.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/TrustIndicator.kt @@ -96,6 +96,7 @@ private fun TrustIndicatorContent(status: Status) { withStyle(SpanStyle(color = second)) { append("#") } withStyle(SpanStyle(color = third)) { append("#") } }, + maxLines = 1, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, )