diff --git a/briar b/briar index b9bac8b6a54da1d83ee03f933b8779aaff398742..93439d9c170b224727e8e1891c106d38162539de 160000 --- a/briar +++ b/briar @@ -1 +1 @@ -Subproject commit b9bac8b6a54da1d83ee03f933b8779aaff398742 +Subproject commit 93439d9c170b224727e8e1891c106d38162539de 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 afbfcbf6dad2a8c744bbca395b963e781b200c85..ae8a42a5330bd5bd7bdca97ea26d9b41bfe9733c 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 @@ -97,10 +97,10 @@ fun loadContactItem( ): 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) }, + contact = contact, + authorInfo = authorInfo, + isConnected = connectionRegistry.isConnected(contact.id), + groupCount = conversationManager.getGroupCount(txn, contact.id), + avatar = authorInfo.avatarHeader?.let { ImageUtils.loadImage(txn, attachmentReader, it) }, ) } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemView.kt index bae6112798841f110379930a4083af5186c9f649..18a3a808561a29f9fa64ec746d67bfbee08531ab 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemView.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItemView.kt @@ -32,8 +32,11 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.res.useResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.text import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis @@ -65,7 +68,11 @@ fun main() = preview( "unread" to 3, "timestamp" to Instant.now().toEpochMilli(), "selected" to false, + "showAvatar" to false, ) { + val avatar = remember { + useResource("images/logo_circle.ico") { loadImageBitmap(it) } + } Column(Modifier.selectableGroup()) { ListItemView(getBooleanParameter("selected")) { val item = ContactItem( @@ -78,7 +85,7 @@ fun main() = preview( isEmpty = getBooleanParameter("isEmpty"), unread = getIntParameter("unread"), timestamp = getLongParameter("timestamp"), - avatar = null, + avatar = if (getBooleanParameter("showAvatar")) avatar else null, ) ContactItemView(item) } 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 index fda601dc0d796f32c5d2c00c219bd6b394382434..6c7ffa1dce018294bca837b7a143c3be4bba4de2 100644 --- 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 @@ -22,7 +22,6 @@ 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 @@ -82,14 +81,12 @@ fun main() = preview( fun ContactItemViewSmall( contactItem: ContactItem, showConnectionState: Boolean = true, + modifier: Modifier = Modifier, ) = Row( horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + 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) } 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 759db1864af83c247c9ac92e16cf68a17eede07e..a31a8015738d497ff3700072d25bc476f19fa658 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 @@ -18,8 +18,6 @@ package org.briarproject.briar.desktop.contact -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -28,8 +26,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface @@ -38,7 +34,6 @@ 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 @@ -55,6 +50,7 @@ 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.ui.VerticallyScrollableArea import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.PreviewUtils.preview import java.time.Instant @@ -126,8 +122,6 @@ fun ContactList( setFilterBy: (String) -> Unit, onContactAdd: () -> Unit, ) { - val scrollState = rememberLazyListState() - Surface( modifier = Modifier.fillMaxHeight().width(COLUMN_WIDTH), color = MaterialTheme.colors.surfaceVariant @@ -146,7 +140,7 @@ fun ContactList( ) } - Box(modifier = Modifier.fillMaxSize()) { + VerticallyScrollableArea(modifier = Modifier.fillMaxSize()) { scrollState -> LazyColumn( state = scrollState, modifier = Modifier @@ -181,11 +175,6 @@ fun ContactList( } } } - - VerticalScrollbar( - adapter = rememberScrollbarAdapter(scrollState), - modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight() - ) } } } 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 deleted file mode 100644 index 7e6ac6a5a97ffb10db2119fa5817f4eda47e4362..0000000000000000000000000000000000000000 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt +++ /dev/null @@ -1,163 +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.forums - -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.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.bramble.api.plugin.event.ContactConnectedEvent -import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent -import org.briarproject.bramble.api.sync.GroupId -import org.briarproject.briar.api.attachment.AttachmentReader -import org.briarproject.briar.api.conversation.ConversationManager -import org.briarproject.briar.api.forum.ForumSharingManager -import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent -import org.briarproject.briar.api.identity.AuthorManager -import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent -import org.briarproject.briar.desktop.contact.ContactItem -import org.briarproject.briar.desktop.threading.BriarExecutors -import org.briarproject.briar.desktop.threading.UiExecutor -import org.briarproject.briar.desktop.utils.ImageUtils.loadImage -import org.briarproject.briar.desktop.utils.clearAndAddAll -import org.briarproject.briar.desktop.utils.removeFirst -import org.briarproject.briar.desktop.utils.replaceFirst -import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel -import org.briarproject.briar.desktop.viewmodel.asList -import org.briarproject.briar.desktop.viewmodel.asState -import org.briarproject.briar.desktop.viewmodel.update -import javax.inject.Inject - -class ForumSharingViewModel @Inject constructor( - private val forumSharingManager: ForumSharingManager, - private val contactManager: ContactManager, - private val authorManager: AuthorManager, - private val conversationManager: ConversationManager, - private val connectionRegistry: ConnectionRegistry, - private val attachmentReader: AttachmentReader, - briarExecutors: BriarExecutors, - lifecycleManager: LifecycleManager, - db: TransactionManager, - eventBus: EventBus, -) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) { - - companion object { - private val LOG = KotlinLogging.logger {} - } - - private lateinit var groupId: GroupId - - private val _currentlySharedWith = mutableStateListOf<ContactItem>() - val currentlySharedWith = _currentlySharedWith.asList() - - private val _sharingInfo = mutableStateOf(SharingInfo(0, 0)) - val sharingInfo = _sharingInfo.asState() - - @UiExecutor - fun setGroupId(groupId: GroupId) { - if (this::groupId.isInitialized && groupId == this.groupId) return - this.groupId = groupId - loadSharedWith() - } - - @UiExecutor - override fun eventOccurred(e: Event) { - when { - e is ForumInvitationResponseReceivedEvent && e.messageHeader.shareableId == groupId && e.messageHeader.wasAccepted() -> - runOnDbThreadWithTransaction(false) { txn -> - val contact = contactManager.getContact(txn, e.contactId) - val authorInfo = authorManager.getAuthorInfo(txn, contact) - val connected = connectionRegistry.isConnected(contact.id) - val item = ContactItem( - contact, - authorInfo, - connected, - conversationManager.getGroupCount(txn, contact.id), // todo: not necessary to be shown here - authorInfo.avatarHeader?.let { loadImage(txn, attachmentReader, it) }, - ) - txn.attach { - _currentlySharedWith.add(item) - _sharingInfo.update { addContact(connected) } - } - } - - e is ContactLeftShareableEvent && e.groupId == groupId -> { - _currentlySharedWith.removeFirst { it.id == e.contactId } - val connected = connectionRegistry.isConnected(e.contactId) - _sharingInfo.update { removeContact(connected) } - } - - e is ContactConnectedEvent -> { - 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.id == e.contactId }) { - it.updateIsConnected(false) - } - if (isMember) _sharingInfo.update { updateContactConnected(false) } - } - } - } - - private fun loadSharedWith() = runOnDbThreadWithTransaction(true) { txn -> - var online = 0 - val list = forumSharingManager.getSharedWith(txn, groupId).map { contact -> - val authorInfo = authorManager.getAuthorInfo(txn, contact) - val connected = connectionRegistry.isConnected(contact.id) - if (connected) online++ - ContactItem( - contact, - authorInfo, - connected, - conversationManager.getGroupCount(txn, contact.id), // todo: not necessary to be shown here - authorInfo.avatarHeader?.let { loadImage(txn, attachmentReader, it) }, - ) - } - txn.attach { - _currentlySharedWith.clearAndAddAll(list) - _sharingInfo.value = SharingInfo(list.size, online) - } - } - - data class SharingInfo(val total: Int, val online: Int) { - fun addContact(connected: Boolean) = copy( - total = total + 1, - online = if (connected) online + 1 else online - ) - - fun removeContact(connected: Boolean) = copy( - total = total - 1, - online = if (connected) online - 1 else online - ) - - fun updateContactConnected(connected: Boolean) = copy( - total = total, - online = if (connected) online + 1 else online - 1 - ) - } -} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt index b8a8adbb4c768c3c7027e6845c905c5f96ed877b..642152e4520067fe6f2496712ffcaaaa56567f3f 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt @@ -53,6 +53,9 @@ import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.contact.ContactDropDown.State.CLOSED import org.briarproject.briar.desktop.contact.ContactDropDown.State.MAIN +import org.briarproject.briar.desktop.forums.sharing.ForumSharingActionDrawerContent +import org.briarproject.briar.desktop.forums.sharing.ForumSharingStatusDrawerContent +import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider import org.briarproject.briar.desktop.ui.getInfoDrawerHandler @@ -139,8 +142,23 @@ private fun GroupConversationHeader( onClick = { close() infoDrawerHandler.open { - ForumSharingDrawerContent( - groupId = groupItem.id, + ForumSharingActionDrawerContent( + close = infoDrawerHandler::close, + viewModel = forumSharingViewModel, + ) + } + } + ) { + Text( + i18n("forum.sharing.action.title"), + style = MaterialTheme.typography.body2, + ) + } + DropdownMenuItem( + onClick = { + close() + infoDrawerHandler.open { + ForumSharingStatusDrawerContent( close = infoDrawerHandler::close, viewModel = forumSharingViewModel, ) diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt index 3351fa255a49ae5333b45d0bd201e27b4a1ccf98..08aa3bbea804179f769b625b394164cca261adea 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt @@ -40,6 +40,7 @@ import org.briarproject.briar.api.forum.ForumManager import org.briarproject.briar.api.forum.ForumPostHeader import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent import org.briarproject.briar.client.MessageTreeImpl +import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.threading.UiExecutor import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/sharing/ForumSharingActionDrawerContent.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/sharing/ForumSharingActionDrawerContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..815e43f9abbd92691e28cdb969373cc0b8619c38 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/sharing/ForumSharingActionDrawerContent.kt @@ -0,0 +1,174 @@ +/* + * 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.forums.sharing + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button +import androidx.compose.material.Checkbox +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.ERROR +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.INVITE_RECEIVED +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.INVITE_SENT +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.NOT_SUPPORTED +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING +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.ui.VerticallyScrollableArea +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n + +@Composable +fun ForumSharingActionDrawerContent( + close: () -> Unit, + viewModel: ForumSharingViewModel, +) = Column { + Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { + IconButton( + icon = Icons.Filled.Close, + contentDescription = i18n("access.forum.sharing.action.close"), + onClick = close, + modifier = Modifier.padding(start = 24.dp).size(24.dp).align(Alignment.CenterVertically) + ) + Text( + text = i18n("forum.sharing.action.title"), + modifier = Modifier.align(Alignment.CenterVertically).padding(start = 16.dp), + style = MaterialTheme.typography.h3, + ) + } + HorizontalDivider() + Box(Modifier.fillMaxWidth().weight(1f)) { + if (viewModel.contactList.value.isEmpty()) { + // todo: this might be shown to the user while the list is still loading + Text( + text = i18n("forum.sharing.action.no_contacts"), + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(8.dp).align(Alignment.Center), + ) + } else { + VerticallyScrollableArea { scrollState -> + LazyColumn(state = scrollState) { + items( + items = viewModel.contactList.value, + key = { it.contactItem.id }, + ) { shareableContactItem -> + ForumSharingActionListItem( + shareableContactItem = shareableContactItem, + shareable = shareableContactItem.status == SHAREABLE, + selected = viewModel.isShareableSelected(shareableContactItem), + onToggle = { viewModel.toggleShareable(shareableContactItem) }, + ) + } + } + } + } + } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().padding(8.dp), + ) { + val shareForum = { + if (viewModel.buttonEnabled.value) { + viewModel.shareForum() + close() + } + } + + TextField( + value = viewModel.sharingMessage.value, + onValueChange = viewModel::setSharingMessage, + onEnter = shareForum, + placeholder = { + Text( + text = i18n("forum.sharing.action.add_message"), + style = MaterialTheme.typography.body1, + ) + }, + modifier = Modifier.fillMaxWidth().heightIn(max = 100.dp) + ) + Button( + onClick = shareForum, + modifier = Modifier.fillMaxWidth(), + enabled = viewModel.buttonEnabled.value, + ) { + Text(i18n("forum.sharing.action.title")) + } + } +} + +@Composable +private fun ForumSharingActionListItem( + shareableContactItem: ForumSharingViewModel.ShareableContactItem, + shareable: Boolean, + selected: Boolean, + onToggle: () -> Unit, +) = ListItemView( + selected = if (shareable) selected else null, + onSelect = onToggle, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = selected, + onCheckedChange = { onToggle() }, + enabled = shareable + ) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(8.dp) + ) { + ContactItemViewSmall( + shareableContactItem.contactItem, + showConnectionState = false, + ) + if (!shareable) { + Text( + when (shareableContactItem.status) { + SHAREABLE -> "" + SHARING -> i18n("forum.sharing.action.status.already_shared") + INVITE_SENT -> i18n("forum.sharing.action.status.already_invited") + INVITE_RECEIVED -> i18n("forum.sharing.action.status.invite_received") + NOT_SUPPORTED -> i18n("forum.sharing.action.status.not_supported") + ERROR -> i18n("forum.sharing.action.status.error") + }, + style = MaterialTheme.typography.caption, + ) + } + } + } +} 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/sharing/ForumSharingStatusDrawerContent.kt similarity index 88% rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/sharing/ForumSharingStatusDrawerContent.kt index 3bfa7535a28e419000afaa8cf083c5f682b65b78..e24d697db7567836d0c57060d07f9f3454a0643a 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/sharing/ForumSharingStatusDrawerContent.kt @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -package org.briarproject.briar.desktop.forums +package org.briarproject.briar.desktop.forums.sharing import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box @@ -37,11 +37,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Info 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 org.briarproject.bramble.api.sync.GroupId import org.briarproject.briar.desktop.contact.ContactItemViewSmall import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider @@ -49,14 +47,10 @@ import org.briarproject.briar.desktop.ui.ListItemView import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @Composable -fun ForumSharingDrawerContent( - groupId: GroupId, +fun ForumSharingStatusDrawerContent( close: () -> Unit, viewModel: ForumSharingViewModel, ) = Column { - LaunchedEffect(groupId) { - viewModel.setGroupId(groupId) - } Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { IconButton( icon = Icons.Filled.Close, @@ -88,7 +82,8 @@ fun ForumSharingDrawerContent( } HorizontalDivider() Box(Modifier.fillMaxSize()) { - if (viewModel.currentlySharedWith.isEmpty()) { + if (viewModel.currentlySharedWith.value.isEmpty()) { + // todo: this might be shown to the user while the list is still loading Text( text = i18n("forum.sharing.status.nobody"), style = MaterialTheme.typography.body1, @@ -96,9 +91,12 @@ fun ForumSharingDrawerContent( ) } else { LazyColumn { - items(viewModel.currentlySharedWith) { contactItem -> + items(viewModel.currentlySharedWith.value) { contactItem -> ListItemView { - ContactItemViewSmall(contactItem) + ContactItemViewSmall( + contactItem, + modifier = Modifier.padding(8.dp) + ) } } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/sharing/ForumSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/sharing/ForumSharingViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..11c07d0b493a54fb9d8b7b8039a57b7b327db92a --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/sharing/ForumSharingViewModel.kt @@ -0,0 +1,223 @@ +/* + * 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.forums.sharing + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import mu.KotlinLogging +import org.briarproject.bramble.api.connection.ConnectionRegistry +import org.briarproject.bramble.api.contact.ContactId +import org.briarproject.bramble.api.contact.ContactManager +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.bramble.api.plugin.event.ContactConnectedEvent +import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.briar.api.attachment.AttachmentReader +import org.briarproject.briar.api.conversation.ConversationManager +import org.briarproject.briar.api.forum.ForumSharingManager +import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent +import org.briarproject.briar.api.identity.AuthorManager +import org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_TEXT_LENGTH +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING +import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent +import org.briarproject.briar.desktop.contact.ContactItem +import org.briarproject.briar.desktop.contact.ContactsViewModel +import org.briarproject.briar.desktop.threading.BriarExecutors +import org.briarproject.briar.desktop.threading.UiExecutor +import org.briarproject.briar.desktop.utils.InternationalizationUtils +import org.briarproject.briar.desktop.viewmodel.asState +import org.briarproject.briar.desktop.viewmodel.update +import javax.inject.Inject + +class ForumSharingViewModel @Inject constructor( + private val forumSharingManager: ForumSharingManager, + contactManager: ContactManager, + authorManager: AuthorManager, + conversationManager: ConversationManager, + private val connectionRegistry: ConnectionRegistry, + attachmentReader: AttachmentReader, + briarExecutors: BriarExecutors, + lifecycleManager: LifecycleManager, + db: TransactionManager, + eventBus: EventBus, +) : ContactsViewModel( + contactManager, + authorManager, + conversationManager, + connectionRegistry, + attachmentReader, + briarExecutors, + lifecycleManager, + db, + eventBus, +) { + + companion object { + private val LOG = KotlinLogging.logger {} + } + + private lateinit var groupId: GroupId + + private val _sharingStatus = mutableStateOf(emptyMap<ContactId, SharingStatus>()) + private val _shareableSelected = mutableStateOf(emptySet<ContactId>()) + private val _sharingMessage = mutableStateOf("") + + val currentlySharedWith = derivedStateOf { + _contactList.filter { _sharingStatus.value[it.id] == SHARING } + } + + data class ShareableContactItem(val status: SharingStatus, val contactItem: ContactItem) + + val contactList = derivedStateOf { + _contactList.mapNotNull { + _sharingStatus.value[it.id]?.let { status -> + ShareableContactItem(status, it) + } + }.sortedWith( + // first all items that are SHAREABLE (false comes before true) + // second non-case-sensitive, alphabetical order on displayName + compareBy({ it.status != SHAREABLE }, { it.contactItem.displayName.lowercase(InternationalizationUtils.locale) }) + ) + } + + val sharingMessage = _sharingMessage.asState() + + val buttonEnabled = derivedStateOf { _shareableSelected.value.isNotEmpty() } + + private val _sharingInfo = mutableStateOf(SharingInfo(0, 0)) + val sharingInfo = _sharingInfo.asState() + + override fun onInit() { + super.onInit() + loadContacts() + } + + @UiExecutor + fun setGroupId(groupId: GroupId) { + if (this::groupId.isInitialized && groupId == this.groupId) return + this.groupId = groupId + reload() + } + + private fun reload() { + _shareableSelected.value = emptySet() + _sharingMessage.value = "" + loadSharingStatus() + } + + @UiExecutor + fun isShareableSelected(shareable: ShareableContactItem) = + _shareableSelected.value.contains(shareable.contactItem.id) + + @UiExecutor + fun toggleShareable(shareable: ShareableContactItem) = + if (isShareableSelected(shareable)) _shareableSelected.value -= shareable.contactItem.id + else _shareableSelected.value += shareable.contactItem.id + + @UiExecutor + fun setSharingMessage(message: String) { + _sharingMessage.value = message.take(MAX_INVITATION_TEXT_LENGTH) + } + + @UiExecutor + fun shareForum() = runOnDbThreadWithTransaction(false) { txn -> + val message = _sharingMessage.value.ifEmpty { null } + _shareableSelected.value.forEach { contactId -> + forumSharingManager.sendInvitation(txn, groupId, contactId, message) + } + txn.attach { reload() } + } + + @UiExecutor + override fun eventOccurred(e: Event?) { + super.eventOccurred(e) + + when { + e is ForumInvitationResponseReceivedEvent && e.messageHeader.shareableId == groupId -> { + if (e.messageHeader.wasAccepted()) { + _sharingStatus.value += e.contactId to SHARING + val connected = connectionRegistry.isConnected(e.contactId) + _sharingInfo.update { addContact(connected) } + } else { + _sharingStatus.value += e.contactId to SHAREABLE + } + } + + e is ContactLeftShareableEvent && e.groupId == groupId -> { + _sharingStatus.value += e.contactId to SHAREABLE + val connected = connectionRegistry.isConnected(e.contactId) + _sharingInfo.update { removeContact(connected) } + } + + e is ContactConnectedEvent -> { + if (_sharingStatus.value[e.contactId] == SHARING) + _sharingInfo.update { updateContactConnected(true) } + } + + e is ContactDisconnectedEvent -> { + if (_sharingStatus.value[e.contactId] == SHARING) + _sharingInfo.update { updateContactConnected(false) } + } + } + } + + override fun loadContactsWithinTransaction(txn: Transaction) { + super.loadContactsWithinTransaction(txn) + if (this::groupId.isInitialized) loadSharingStatus(txn) + } + + private fun loadSharingStatus(): Unit = + runOnDbThreadWithTransaction(true, this::loadSharingStatus) + + private fun loadSharingStatus(txn: Transaction) { + val map = contactManager.getContacts(txn).associate { contact -> + contact.id to forumSharingManager.getSharingStatus(txn, groupId, contact) + } + txn.attach { + val sharing = map.filterValues { it == SHARING }.keys + val online = + sharing.fold(0) { acc, it -> if (connectionRegistry.isConnected(it)) acc + 1 else acc } + _sharingStatus.value = map + _sharingInfo.value = SharingInfo(sharing.size, online) + } + } + + data class SharingInfo(val total: Int, val online: Int) { + fun addContact(connected: Boolean) = copy( + total = total + 1, + online = if (connected) online + 1 else online + ) + + fun removeContact(connected: Boolean) = copy( + total = total - 1, + online = if (connected) online - 1 else online + ) + + fun updateContactConnected(connected: Boolean) = copy( + total = total, + online = if (connected) online + 1 else online - 1 + ) + } +} 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 fc835a87e711ec6f04281cd51a61e7991139fa9b..151d4d2698948d11763647693406114e0d47fc4f 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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -143,6 +144,7 @@ fun ContactDrawerMakeIntro( style = MaterialTheme.typography.body1, ) }, + modifier = Modifier.heightIn(max = 200.dp) ) } Row(Modifier.padding(8.dp).weight(1f, true)) { 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 361ec9f67357c94a74cfa1e7e38f4e2ef378a1b6..5327bb780d5b2e5c4bc5036419804daf9b4a7501 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 @@ -28,6 +28,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager 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.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH import org.briarproject.briar.api.introduction.IntroductionManager import org.briarproject.briar.desktop.contact.ContactItem import org.briarproject.briar.desktop.contact.ContactsViewModel @@ -93,7 +94,7 @@ constructor( } fun setIntroductionMessage(msg: String) { - _introductionMessage.value = msg + _introductionMessage.value = msg.take(MAX_INTRODUCTION_TEXT_LENGTH) } fun makeIntroduction() { diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticallyScrollableArea.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticallyScrollableArea.kt new file mode 100644 index 0000000000000000000000000000000000000000..550aba858b16768da49cc8decc8f96cbe64a8ff3 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticallyScrollableArea.kt @@ -0,0 +1,44 @@ +/* + * 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.VerticalScrollbar +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun VerticallyScrollableArea( + modifier: Modifier = Modifier, + content: @Composable (scrollState: LazyListState) -> Unit, +) = Box(modifier) { + val scrollState = rememberLazyListState() + + content(scrollState) + + VerticalScrollbar( + adapter = rememberScrollbarAdapter(scrollState), + modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight() + ) +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt index 6cccf37d388fb3ca6aa9eaea1ceb4c1b05913237..182c8cda279aa6b15f7f11d93e09319f3716ca68 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt @@ -25,8 +25,8 @@ import dagger.multibindings.IntoMap import org.briarproject.briar.desktop.contact.ContactListViewModel import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel import org.briarproject.briar.desktop.conversation.ConversationViewModel -import org.briarproject.briar.desktop.forums.ForumSharingViewModel import org.briarproject.briar.desktop.forums.ForumViewModel +import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel import org.briarproject.briar.desktop.introduction.IntroductionViewModel import org.briarproject.briar.desktop.login.StartupViewModel import org.briarproject.briar.desktop.navigation.SidebarViewModel diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties index aab31710fae088482310f5ebec092af24e93a785..e00996e2827011be9fafaffdb6f3b0895435e96d 100644 --- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties +++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties @@ -67,6 +67,7 @@ access.forums.unread_count={0, plural, one {one unread posts} other {{0} unread access.forums.last_post_timestamp=last post: {0} access.forums.jump_to_prev_unread=Jump to previous unread post access.forums.jump_to_next_unread=Jump to next unread post +access.forum.sharing.action.close=Close sharing form access.forum.sharing.status.close=Close sharing status # Contacts @@ -133,12 +134,14 @@ forum.sharing.status.info=Any member of a forum can share it with their contacts forum.sharing.status.with=Shared with {0} ({1} online) forum.sharing.status.nobody=Nobody forum.sharing.action.title=Share Forum +forum.sharing.action.add_message=Add a message (optional) forum.sharing.action.choose_contacts=Choose Contacts forum.sharing.action.no_contacts=No contacts yet. You can only share forums with your contacts. forum.sharing.action.status.already_shared=Already sharing forum.sharing.action.status.already_invited=Invitation already sent forum.sharing.action.status.invite_received=Invitation already received forum.sharing.action.status.not_supported=Not supported by this contact +forum.sharing.action.status.error=Error. This is a bug and not your fault # Private Groups groups.card.created=Created by {0} diff --git a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/TestUtils.kt b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/TestUtils.kt index e06f6cde026bb1620b75cffd0e5ca8d722c1855f..6ea5bb0733e2cee4e760425a439f31559ffc0a05 100644 --- a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/TestUtils.kt +++ b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/TestUtils.kt @@ -23,6 +23,9 @@ import org.briarproject.bramble.api.Bytes.compare import org.briarproject.bramble.api.FormatException import org.briarproject.bramble.api.contact.PendingContactId import org.briarproject.bramble.api.crypto.SecretKey +import org.briarproject.bramble.api.event.Event +import org.briarproject.bramble.api.event.EventBus +import org.briarproject.bramble.api.event.EventListener import org.briarproject.bramble.api.identity.Author import org.briarproject.bramble.api.identity.LocalAuthor import org.briarproject.bramble.api.plugin.TransportId @@ -30,6 +33,7 @@ import org.briarproject.bramble.api.properties.TransportProperties import org.briarproject.bramble.api.versioning.event.ClientVersionUpdatedEvent import org.briarproject.briar.api.forum.ForumManager import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent +import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE import org.briarproject.briar.desktop.utils.FileUtils import java.io.IOException import java.nio.file.Files @@ -162,7 +166,7 @@ object TestUtils { creator.getBriarExecutors().onDbThread { val contact = creator.getContactManager().getContact(e.contactId) val sharingManager = creator.getForumSharingManager() - if (sharingManager.canBeShared(forum.id, contact)) + if (sharingManager.getSharingStatus(forum.id, contact) == SHAREABLE) sharingManager.sendInvitation(forum.id, e.contactId, null) } } @@ -170,12 +174,22 @@ object TestUtils { // accept invitation at all contacts drop(1).forEach { app -> - app.getEventBus().addListener { e -> + app.getEventBus().addListenerOnce { e -> if (e is ForumInvitationRequestReceivedEvent) { val contact = app.getContactManager().getContact(e.contactId) app.getForumSharingManager().respondToInvitation(forum, contact, true) + return@addListenerOnce true } + false } } } } + +fun EventBus.addListenerOnce(listener: (Event) -> Boolean) = + addListener(object : EventListener { + override fun eventOccurred(e: Event) { + if (listener(e)) + removeListener(this) + } + }) diff --git a/build.gradle.kts b/build.gradle.kts index ae478abc47fbc734ff1df01ca6f2a2baf32fa5f9..bd1cf57024b4e9c47f815f98883a73f265d754b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,6 @@ buildscript { // keep version here in sync when updating briar dependencies { - classpath("com.android.tools.build:gradle:4.1.3") classpath("ru.vyarus:gradle-animalsniffer-plugin:1.5.3") classpath(files("briar/libs/gradle-witness.jar")) } @@ -39,9 +38,11 @@ buildscript { set("dagger_version", "2.43.2") set("okhttp_version", "3.12.13") set("jackson_version", "2.13.4") - set("tor_version", "0.4.5.14") + set("tor_version", "0.4.7.12") set("obfs4proxy_version", "0.0.14-tor1") set("snowflake_version", "2.3.1") + set("jsoup_version", "1.15.3") + set("bouncy_castle_version", "1.71") set("junit_version", "4.13.2") set("jmock_version", "2.12.0") }