Skip to content
Snippets Groups Projects
Verified Commit 1c1735d2 authored by Mikolai Gütschow's avatar Mikolai Gütschow
Browse files

show connection status for revealed contacts and correctly update member list on events

also allow to react on group invitations, thereby fixes #116
parent dafff906
No related branches found
No related tags found
1 merge request!326Show member list for private groups
Pipeline #14206 passed
Showing
with 193 additions and 76 deletions
......@@ -35,6 +35,7 @@ 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.privategroup.sharing.GroupMemberItem
import org.briarproject.briar.desktop.ui.TrustIndicatorShort
import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
......@@ -83,6 +84,18 @@ fun ContactItemViewSmall(
modifier = modifier,
)
@Composable
fun ContactItemViewSmall(
groupMemberItem: GroupMemberItem,
modifier: Modifier = Modifier,
) = ContactItemViewSmall(
displayName = groupMemberItem.displayName,
authorId = groupMemberItem.authorId,
authorInfo = groupMemberItem.authorInfo,
isConnected = groupMemberItem.isConnected,
modifier = modifier,
)
@Composable
fun ContactItemViewSmall(
displayName: String,
......
......@@ -58,11 +58,14 @@ import org.briarproject.briar.api.messaging.MessagingManager
import org.briarproject.briar.api.messaging.PrivateMessage
import org.briarproject.briar.api.messaging.PrivateMessageFactory
import org.briarproject.briar.api.messaging.PrivateMessageHeader
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager
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.BLOG
import org.briarproject.briar.desktop.conversation.ConversationRequestItem.RequestType.FORUM
import org.briarproject.briar.desktop.conversation.ConversationRequestItem.RequestType.GROUP
import org.briarproject.briar.desktop.conversation.ConversationRequestItem.RequestType.INTRODUCTION
import org.briarproject.briar.desktop.forum.sharing.ForumInvitationSentEvent
import org.briarproject.briar.desktop.threading.BriarExecutors
......@@ -92,6 +95,7 @@ constructor(
private val conversationManager: ConversationManager,
private val introductionManager: IntroductionManager,
private val forumSharingManager: ForumSharingManager,
private val groupInvitationManager: GroupInvitationManager,
private val messagingManager: MessagingManager,
private val privateMessageFactory: PrivateMessageFactory,
briarExecutors: BriarExecutors,
......@@ -431,20 +435,31 @@ constructor(
introductionManager.respondToIntroduction(txn, _contactId.value!!, item.sessionId, accept)
FORUM -> {
if (desktopFeatureFlags.shouldEnableForums()) {
forumSharingManager.respondToInvitation(
/* txn = */ txn,
/* c = */ _contactId.value!!,
/* id = */ item.sessionId,
/* accept = */ accept
)
} else {
LOG.e { "Forum requests are not supported for this build." }
require(desktopFeatureFlags.shouldEnableForums()) {
"Forum requests are not supported for this build." // NON-NLS
}
forumSharingManager.respondToInvitation(
/* txn = */ txn,
/* c = */ _contactId.value!!,
/* id = */ item.sessionId,
/* accept = */ accept
)
}
GROUP -> {
require(desktopFeatureFlags.shouldEnablePrivateGroups()) {
"Private group requests are not supported for this build." // NON-NLS
}
groupInvitationManager.respondToInvitation(
/* txn = */ txn,
/* c = */ _contactId.value!!,
/* s = */ item.sessionId,
/* accept = */ accept
)
}
else ->
LOG.e { "Only introduction and forum requests are supported for the time being." }
BLOG ->
LOG.e { "Blogs are not supported for the time being." }
}
// reload all messages to also show request response message
// todo: might be better to have an event to react to, also for (other) outgoing messages
......
......@@ -31,6 +31,7 @@ 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.conversation.ConversationManager
import org.briarproject.briar.api.forum.ForumSharingManager
import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent
......@@ -110,9 +111,12 @@ class ForumSharingViewModel @Inject constructor(
}
override fun reload() {
super.reload()
_shareableSelected.value = emptySet()
_sharingMessage.value = ""
val groupId = _groupId ?: return
runOnDbThreadWithTransaction(true) { txn ->
loadSharingStatus(txn, groupId)
}
}
@UiExecutor
......@@ -176,13 +180,12 @@ class ForumSharingViewModel @Inject constructor(
}
}
override fun loadSharingStatus(txn: Transaction) {
val groupId = _groupId ?: return
fun loadSharingStatus(txn: Transaction, groupId: GroupId) {
val map = contactManager.getContacts(txn).associate { contact ->
contact.id to forumSharingManager.getSharingStatus(txn, groupId, contact)
}
val sharing = map.filterValues { it == SHARING }.keys
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
......
/*
* 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 org.briarproject.bramble.api.connection.ConnectionRegistry
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.privategroup.GroupMember
import org.briarproject.briar.desktop.utils.UiUtils.getContactDisplayName
data class GroupMemberItem(
val authorId: AuthorId,
val authorInfo: AuthorInfo,
private val name: String,
val isCreator: Boolean,
val contactId: ContactId?,
val isConnected: Boolean?,
) {
val displayName = getContactDisplayName(name, authorInfo.alias)
val isVisibleContact = contactId != null
constructor(
groupMember: GroupMember,
isConnected: Boolean?,
) : this(
authorId = groupMember.author.id,
authorInfo = groupMember.authorInfo,
name = groupMember.author.name,
isCreator = groupMember.isCreator,
contactId = groupMember.contactId,
isConnected = isConnected
)
fun updateIsConnected(c: Boolean) =
copy(isConnected = c)
fun updateAuthorInfo(authorInfo: AuthorInfo) =
copy(authorInfo = authorInfo)
}
fun loadGroupMemberItem(groupMember: GroupMember, connectionRegistry: ConnectionRegistry) =
GroupMemberItem(
groupMember,
groupMember.contactId?.let { connectionRegistry.isConnected(it) }
)
......@@ -40,14 +40,12 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically
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(
......@@ -85,7 +83,7 @@ fun PrivateGroupMemberDrawerContent(
}
HorizontalDivider()
LazyColumn(Modifier.fillMaxSize()) {
items(viewModel.members) { groupMember ->
items(viewModel.members.value) { groupMember ->
PrivateGroupMemberListItem(groupMember)
}
}
......@@ -93,26 +91,17 @@ fun PrivateGroupMemberDrawerContent(
@Composable
private fun PrivateGroupMemberListItem(
groupMember: GroupMember,
groupMember: GroupMemberItem,
) = ListItemView {
Column(
verticalArrangement = 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,
)
ContactItemViewSmall(groupMember)
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)
),
else i18nF("group.member.created_contact", groupMember.displayName),
style = MaterialTheme.typography.caption,
)
}
......
......@@ -24,18 +24,22 @@ import androidx.compose.runtime.mutableStateOf
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.contact.event.ContactAddedEvent
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent
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.identity.IdentityManager
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.conversation.ConversationManager
import org.briarproject.briar.api.identity.AuthorManager
import org.briarproject.briar.api.privategroup.GroupMember
import org.briarproject.briar.api.privategroup.JoinMessageHeader
import org.briarproject.briar.api.privategroup.PrivateGroupManager
import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent
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
......@@ -49,7 +53,7 @@ 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.utils.replaceFirst
import org.briarproject.briar.desktop.viewmodel.asState
import org.briarproject.briar.desktop.viewmodel.update
import javax.inject.Inject
......@@ -57,6 +61,7 @@ import javax.inject.Inject
class PrivateGroupSharingViewModel @Inject constructor(
private val privateGroupManager: PrivateGroupManager,
private val privateGroupInvitationManager: GroupInvitationManager,
private val identityManager: IdentityManager,
contactManager: ContactManager,
authorManager: AuthorManager,
conversationManager: ConversationManager,
......@@ -80,8 +85,20 @@ class PrivateGroupSharingViewModel @Inject constructor(
private val _shareableSelected = mutableStateOf(emptySet<ContactId>())
private val _sharingMessage = mutableStateOf("")
private val _members = mutableStateListOf<GroupMember>()
val members = _members.asList()
private val _isCreator = mutableStateOf(false)
val isCreator = _isCreator.asState()
private val _members = mutableStateListOf<GroupMemberItem>()
val members = derivedStateOf {
_members.sortedWith(
// first creator of the group (false comes before true)
// second non-case-sensitive, alphabetical order on displayName
compareBy(
{ !it.isCreator },
{ it.displayName.lowercase(InternationalizationUtils.locale) }
)
)
}
data class ShareableContactItem(val status: SharingStatus, val contactItem: ContactItem)
......@@ -110,14 +127,24 @@ class PrivateGroupSharingViewModel @Inject constructor(
}
override fun reload() {
super.reload()
_shareableSelected.value = emptySet()
_sharingMessage.value = ""
reloadMembers()
}
private fun reloadMembers() {
val groupId = _groupId ?: return
runOnDbThreadWithTransaction(true) { txn ->
val members = privateGroupManager.getMembers(txn, _groupId!!)
val isCreator =
privateGroupManager.getPrivateGroup(txn, groupId).creator == identityManager.getLocalAuthor(txn)
val members = privateGroupManager.getMembers(txn, groupId).map {
loadGroupMemberItem(it, connectionRegistry)
}
txn.attach {
_isCreator.value = isCreator
_members.clearAndAddAll(members)
}
loadSharingStatus(txn, groupId, members, isCreator)
}
}
......@@ -152,49 +179,70 @@ class PrivateGroupSharingViewModel @Inject constructor(
val groupId = _groupId ?: return
when {
e is GroupInvitationResponseReceivedEvent && 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
}
// todo: is there any similar leave event we could react to?
e is GroupMessageAddedEvent && e.groupId == groupId && e.header is JoinMessageHeader -> {
reloadMembers()
}
// todo: update/reload member list on member join and leave(?)
// e is GroupMessageAddedEvent && e.groupId == groupId && e.header is JoinMessageHeader
e is ContactAddedEvent || e is ContactRemovedEvent -> {
// the newly added or removed contact may be member of the private group
reloadMembers()
}
// todo: update member list on contact alias/avatar changed (may be member)
// todo: any way of coupling member list to contact list for members that are actually contacts?
// todo: those could be moved to GroupSharingViewModel
// todo: test when leaving groups is implemented
e is ContactLeftShareableEvent && e.groupId == groupId -> {
_sharingStatus.value += e.contactId to SHAREABLE
if (_isCreator.value)
_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)
if (_sharingStatus.value[e.contactId] == SHARING) {
_sharingInfo.update { updateContactConnected(true) }
_members.replaceFirst({ it.contactId == e.contactId }) { it.updateIsConnected(true) }
}
}
e is ContactDisconnectedEvent -> {
if (_sharingStatus.value[e.contactId] == SHARING)
if (_sharingStatus.value[e.contactId] == SHARING) {
_sharingInfo.update { updateContactConnected(false) }
_members.replaceFirst({ it.contactId == e.contactId }) { it.updateIsConnected(false) }
}
}
}
}
override fun loadSharingStatus(txn: Transaction) {
val groupId = _groupId ?: return
val map = contactManager.getContacts(txn).associate { contact ->
contact.id to privateGroupInvitationManager.getSharingStatus(txn, contact, groupId)
}
txn.attach {
private fun loadSharingStatus(
txn: Transaction,
groupId: GroupId,
members: List<GroupMemberItem>,
isCreator: Boolean,
) {
val contacts = contactManager.getContacts(txn)
if (isCreator) {
val map = contacts.associate { contact ->
contact.id to privateGroupInvitationManager.getSharingStatus(txn, contact, groupId)
}
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)
txn.attach {
val online =
sharing.fold(0) { acc, it -> if (connectionRegistry.isConnected(it)) acc + 1 else acc }
_sharingStatus.value = map
_sharingInfo.value = SharingInfo(sharing.size, online)
}
} else {
val sharing = members.mapNotNull { it.contactId }
val map = sharing.associateWith { SHARING }
txn.attach {
val online =
sharing.fold(0) { acc, it -> if (connectionRegistry.isConnected(it)) acc + 1 else acc }
_sharingStatus.value = map
_sharingInfo.value = SharingInfo(sharing.size, online)
}
}
}
}
......@@ -21,7 +21,6 @@ package org.briarproject.briar.desktop.threadedgroup.sharing
import androidx.compose.runtime.mutableStateOf
import org.briarproject.bramble.api.connection.ConnectionRegistry
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.EventBus
import org.briarproject.bramble.api.lifecycle.LifecycleManager
......@@ -69,19 +68,7 @@ abstract class ThreadedGroupSharingViewModel(
reload()
}
protected open fun reload() {
loadSharingStatus()
}
override fun loadContactsWithinTransaction(txn: Transaction) {
super.loadContactsWithinTransaction(txn)
if (_groupId != null) loadSharingStatus(txn)
}
private fun loadSharingStatus(): Unit =
runOnDbThreadWithTransaction(true, this::loadSharingStatus)
protected abstract fun loadSharingStatus(txn: Transaction)
protected abstract fun reload()
data class SharingInfo(val total: Int, val online: Int) {
fun addContact(connected: Boolean) = copy(
......
/*
* 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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment