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 d3af262321545cff1c6faf4873e6c19a39c37801..410d2938d7d38b5cb1bf686f9400eac35a487809 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 @@ -1,6 +1,6 @@ /* * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project + * Copyright (C) 2021-2023 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 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 b602a5127b09693394e518931b2dc861c6b668ad..6e8549b8cf481ba2851ff23698e60c5994e73218 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 @@ -33,6 +33,7 @@ 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 import org.briarproject.briar.api.identity.AuthorInfo.Status import org.briarproject.briar.desktop.ui.TrustIndicatorShort import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @@ -74,13 +75,28 @@ fun ContactItemViewSmall( contactItem: ContactItem, showConnectionState: Boolean = true, modifier: Modifier = Modifier, +) = ContactItemViewSmall( + displayName = contactItem.displayName, + authorId = contactItem.authorId, + authorInfo = contactItem.authorInfo, + isConnected = if (showConnectionState) contactItem.isConnected else null, + modifier = modifier, +) + +@Composable +fun ContactItemViewSmall( + displayName: String, + authorId: AuthorId, + authorInfo: AuthorInfo, + isConnected: Boolean? = null, + modifier: Modifier = Modifier, ) = Row( horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxWidth() .semantics { - text = getDescription(contactItem, showConnectionState) + text = getDescription(displayName, isConnected) } ) { Row( @@ -88,32 +104,32 @@ fun ContactItemViewSmall( horizontalArrangement = spacedBy(8.dp), modifier = Modifier.weight(1f, fill = true), ) { - ProfileCircle(27.dp, contactItem) + ProfileCircle(20.dp, authorId, authorInfo) Text( modifier = Modifier.weight(1f, fill = false), - text = contactItem.displayName, + text = displayName, style = MaterialTheme.typography.body1, maxLines = 3, overflow = Ellipsis, ) - TrustIndicatorShort(contactItem.trustLevel) + TrustIndicatorShort(authorInfo.status) } - if (showConnectionState) + if (isConnected != null) ConnectionIndicator( modifier = Modifier.requiredSize(12.dp), - isConnected = contactItem.isConnected + isConnected = isConnected ) } private fun getDescription( - contactItem: ContactItem, - showConnectionState: Boolean, + displayName: String, + isConnected: Boolean?, ) = buildBlankAnnotatedString { - append(i18nF("access.contact.with_name", contactItem.displayName)) + append(i18nF("access.contact.with_name", displayName)) // todo: trust level! - if (showConnectionState) + if (isConnected != null) appendCommaSeparated( - if (contactItem.isConnected) i18n("access.contact.connected.yes") + if (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/privategroup/conversation/PrivateGroupDropdownMenu.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupDropdownMenu.kt index 571b132e8f40a1305548cb274501e518e0924e2b..bc50e66a90aee257fb9ee2912eda2c17d08da637 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupDropdownMenu.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupDropdownMenu.kt @@ -23,6 +23,7 @@ import androidx.compose.material.DropdownMenuItem import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupMemberDrawerContent import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupSharingViewModel import org.briarproject.briar.desktop.ui.getInfoDrawerHandler import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @@ -32,17 +33,33 @@ fun PrivateGroupDropdownMenu( privateGroupSharingViewModel: PrivateGroupSharingViewModel, expanded: Boolean, onClose: () -> Unit, - onLeaveForumClick: () -> Unit, + onLeaveForumClick: () -> Unit, // todo: rename ) = DropdownMenu( expanded = expanded, onDismissRequest = onClose, ) { val infoDrawerHandler = getInfoDrawerHandler() + DropdownMenuItem( + onClick = { + onClose() + infoDrawerHandler.open { + PrivateGroupMemberDrawerContent( + close = infoDrawerHandler::close, + viewModel = privateGroupSharingViewModel, + ) + } + } + ) { + Text( + i18n("group.member.title"), + style = MaterialTheme.typography.body2, + ) + } // DropdownMenuItem( // onClick = { // onClose() // infoDrawerHandler.open { -// ForumSharingActionDrawerContent( +// ForumSharingStatusDrawerContent( // close = infoDrawerHandler::close, // viewModel = forumSharingViewModel, // ) @@ -50,35 +67,19 @@ fun PrivateGroupDropdownMenu( // } // ) { // Text( -// i18n("forum.sharing.action.title"), +// i18n("forum.sharing.status.title"), // style = MaterialTheme.typography.body2, // ) // } // DropdownMenuItem( // onClick = { // onClose() -// infoDrawerHandler.open { -// ForumSharingStatusDrawerContent( -// close = infoDrawerHandler::close, -// viewModel = forumSharingViewModel, -// ) -// } +// onLeaveForumClick() // } // ) { // Text( -// i18n("forum.sharing.status.title"), +// i18n("forum.leave.title"), // todo: change // style = MaterialTheme.typography.body2, // ) // } - DropdownMenuItem( - onClick = { - onClose() - onLeaveForumClick() - } - ) { - Text( - i18n("forum.leave.title"), - style = MaterialTheme.typography.body2, - ) - } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupMemberDrawerContent.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupMemberDrawerContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..45c8ea00a0bcf575ab754305ddb4bd488b79b340 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupMemberDrawerContent.kt @@ -0,0 +1,121 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2023 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.privategroup.sharing + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +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.identity.AuthorInfo.Status.OURSELVES +import org.briarproject.briar.api.privategroup.GroupMember +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.utils.InternationalizationUtils.i18nF +import org.briarproject.briar.desktop.utils.UiUtils.getContactDisplayName + +@Composable +fun PrivateGroupMemberDrawerContent( + close: () -> Unit, + viewModel: PrivateGroupSharingViewModel, +) = Column { + Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { + IconButton( + icon = Icons.Filled.Close, + contentDescription = i18n("access.group.member.close"), + onClick = close, + modifier = Modifier.padding(start = 24.dp).size(24.dp).align(Alignment.CenterVertically) + ) + Text( + text = i18n("group.member.title"), + modifier = Modifier.align(Alignment.CenterVertically).padding(start = 16.dp), + style = MaterialTheme.typography.h3, + ) + } + HorizontalDivider() + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + modifier = Modifier.padding(8.dp), + ) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Text( + text = i18n("group.member.info"), + style = MaterialTheme.typography.body2, + ) + } + HorizontalDivider() + LazyColumn(Modifier.fillMaxSize()) { + items(viewModel.members) { groupMember -> + PrivateGroupMemberListItem(groupMember) + } + } +} + +@Composable +private fun PrivateGroupMemberListItem( + groupMember: GroupMember, +) = ListItemView { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(8.dp) + ) { + ContactItemViewSmall( + displayName = groupMember.author.name, + authorId = groupMember.author.id, + authorInfo = groupMember.authorInfo, + // todo: Android shows connection status if contact + isConnected = null, + ) + if (groupMember.isCreator) { + Text( + if (groupMember.authorInfo.status == OURSELVES) i18n("group.member.created_you") + else i18nF( + "group.member.created_contact", + getContactDisplayName(groupMember.author.name, groupMember.authorInfo.alias) + ), + style = MaterialTheme.typography.caption, + ) + } + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt index c6607bc35128b202eceaada1e3fe93705a170d72..729c86059c744707ec1846b4b5ac633a3d2ba0ee 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt @@ -19,6 +19,7 @@ package org.briarproject.briar.desktop.privategroup.sharing 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 @@ -32,9 +33,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.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.privategroup.GroupMember +import org.briarproject.briar.api.privategroup.PrivateGroupManager +import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager 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 @@ -46,12 +49,15 @@ 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.utils.StringUtils.takeUtf8 +import org.briarproject.briar.desktop.utils.clearAndAddAll +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 PrivateGroupSharingViewModel @Inject constructor( - private val forumSharingManager: ForumSharingManager, + private val privateGroupManager: PrivateGroupManager, + private val privateGroupInvitationManager: GroupInvitationManager, contactManager: ContactManager, authorManager: AuthorManager, conversationManager: ConversationManager, @@ -79,9 +85,8 @@ class PrivateGroupSharingViewModel @Inject constructor( private val _shareableSelected = mutableStateOf(emptySet<ContactId>()) private val _sharingMessage = mutableStateOf("") - val currentlySharedWith = derivedStateOf { - _contactList.filter { _sharingStatus.value[it.id] == SHARING } - } + private val _members = mutableStateListOf<GroupMember>() + val members = _members.asList() data class ShareableContactItem(val status: SharingStatus, val contactItem: ContactItem) @@ -113,6 +118,12 @@ class PrivateGroupSharingViewModel @Inject constructor( super.reload() _shareableSelected.value = emptySet() _sharingMessage.value = "" + runOnDbThreadWithTransaction(true) { txn -> + val members = privateGroupManager.getMembers(txn, _groupId!!) + txn.attach { + _members.clearAndAddAll(members) + } + } } @UiExecutor @@ -129,12 +140,13 @@ class PrivateGroupSharingViewModel @Inject constructor( _sharingMessage.value = message.takeUtf8(MAX_INVITATION_TEXT_LENGTH) } + // todo: only possible if group creator @UiExecutor fun shareForum() = runOnDbThreadWithTransaction(false) { txn -> val groupId = _groupId ?: return@runOnDbThreadWithTransaction val message = _sharingMessage.value.ifEmpty { null } _shareableSelected.value.forEach { contactId -> - forumSharingManager.sendInvitation(txn, groupId, contactId, message) + // privateGroupInvitationManager.sendInvitation(txn, groupId, contactId, message) } txn.attach { reload() } } @@ -145,7 +157,7 @@ class PrivateGroupSharingViewModel @Inject constructor( val groupId = _groupId ?: return when { - e is ForumInvitationResponseReceivedEvent && e.messageHeader.shareableId == groupId -> { + e is GroupInvitationResponseReceivedEvent && e.messageHeader.shareableId == groupId -> { if (e.messageHeader.wasAccepted()) { _sharingStatus.value += e.contactId to SHARING val connected = connectionRegistry.isConnected(e.contactId) @@ -155,6 +167,10 @@ class PrivateGroupSharingViewModel @Inject constructor( } } + // todo: update/reload member list on member join and leave(?) + // e is GroupMessageAddedEvent && e.groupId == groupId && e.header is JoinMessageHeader + + // todo: those could be moved to GroupSharingViewModel e is ContactLeftShareableEvent && e.groupId == groupId -> { _sharingStatus.value += e.contactId to SHAREABLE val connected = connectionRegistry.isConnected(e.contactId) @@ -176,7 +192,7 @@ class PrivateGroupSharingViewModel @Inject constructor( override fun loadSharingStatus(txn: Transaction) { val groupId = _groupId ?: return val map = contactManager.getContacts(txn).associate { contact -> - contact.id to forumSharingManager.getSharingStatus(txn, groupId, contact) + contact.id to privateGroupInvitationManager.getSharingStatus(txn, contact, groupId) } txn.attach { val sharing = map.filterValues { it == SHARING }.keys diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties index 80dd5e2f3a09ef3dc8aa89de5693aaa73948f896..b1628600406d93b6d8f552441b106dc682795b3f 100644 --- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties +++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties @@ -89,6 +89,7 @@ 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 +access.group.member.close=Close member list access.group.list=private group list access.group.reply.close=Close reply @@ -190,7 +191,10 @@ group.leave.title=Leave Group group.leave.dialog.title=Confirm Leaving Group group.leave.dialog.message=Are you sure that you want to leave this private group? group.leave.dialog.button=Leave - +group.member.title=Member List +group.member.info=Only the creator can invite new members to the group. Below are all current members of the group. +group.member.created_you=You created the group +group.member.created_contact={0} created the group # Introduction introduction.introduce=Make Introduction