diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/ForumScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/ForumScreen.kt index 803705e23d97b1f39bf4e916d76210732dcbd048..ec14f6f62386a6c789a34cbde6f671c5798407ed 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/ForumScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/ForumScreen.kt @@ -20,6 +20,7 @@ package org.briarproject.briar.desktop.forum import androidx.compose.runtime.Composable import org.briarproject.briar.desktop.forum.conversation.ForumDropdownMenu +import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupScreen import org.briarproject.briar.desktop.viewmodel.viewModel @@ -29,7 +30,8 @@ fun ForumScreen( ) = ThreadedGroupScreen( strings = ForumStrings, viewModel = viewModel, - dropdownMenu = { forumSharingViewModel, expanded, onClose, onLeaveForumClick -> + dropdownMenu = { sharingViewModel, expanded, onClose, onLeaveForumClick -> + val forumSharingViewModel = sharingViewModel as ForumSharingViewModel ForumDropdownMenu(forumSharingViewModel, expanded, onClose, onLeaveForumClick) } ) diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/sharing/ForumSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/sharing/ForumSharingViewModel.kt index cf041bf7816889f07d81e2651fdb646c5f30b206..98683c9421de7a2bcd53c6a812831b43010936d9 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/sharing/ForumSharingViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/sharing/ForumSharingViewModel.kt @@ -31,7 +31,6 @@ 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 @@ -42,7 +41,7 @@ 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.threadedgroup.sharing.ThreadedGroupSharingViewModel import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.threading.UiExecutor import org.briarproject.briar.desktop.utils.InternationalizationUtils @@ -61,7 +60,7 @@ class ForumSharingViewModel @Inject constructor( lifecycleManager: LifecycleManager, db: TransactionManager, eventBus: EventBus, -) : ContactsViewModel( +) : ThreadedGroupSharingViewModel( contactManager, authorManager, conversationManager, @@ -76,8 +75,6 @@ class ForumSharingViewModel @Inject constructor( private val LOG = KotlinLogging.logger {} } - private var _groupId: GroupId? = null - private val _sharingStatus = mutableStateOf(emptyMap<ContactId, SharingStatus>()) private val _shareableSelected = mutableStateOf(emptySet<ContactId>()) private val _sharingMessage = mutableStateOf("") @@ -107,25 +104,15 @@ class ForumSharingViewModel @Inject constructor( 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 (_groupId == groupId) return - _groupId = groupId - reload() - } - - private fun reload() { + override fun reload() { + super.reload() _shareableSelected.value = emptySet() _sharingMessage.value = "" - loadSharingStatus() } @UiExecutor @@ -189,15 +176,7 @@ class ForumSharingViewModel @Inject constructor( } } - override fun loadContactsWithinTransaction(txn: Transaction) { - super.loadContactsWithinTransaction(txn) - if (_groupId != null) loadSharingStatus(txn) - } - - private fun loadSharingStatus(): Unit = - runOnDbThreadWithTransaction(true, this::loadSharingStatus) - - private fun loadSharingStatus(txn: Transaction) { + override fun loadSharingStatus(txn: Transaction) { val groupId = _groupId ?: return val map = contactManager.getContacts(txn).associate { contact -> contact.id to forumSharingManager.getSharingStatus(txn, groupId, contact) @@ -210,21 +189,4 @@ class ForumSharingViewModel @Inject constructor( _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/privategroup/PrivateGroupScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt index 286b65482b6923522ead7c550a5578b761e18189..522783d3037f4574d015b901a1f151e8c3f33b05 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt @@ -19,6 +19,8 @@ package org.briarproject.briar.desktop.privategroup import androidx.compose.runtime.Composable +import org.briarproject.briar.desktop.privategroup.conversation.PrivateGroupDropdownMenu +import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupSharingViewModel import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupScreen import org.briarproject.briar.desktop.viewmodel.viewModel @@ -28,8 +30,8 @@ fun PrivateGroupScreen( ) = ThreadedGroupScreen( strings = PrivateGroupStrings, viewModel = viewModel, - dropdownMenu = { forumSharingViewModel, expanded, onClose, onLeaveForumClick -> - // todo: uncomment when adapted to groups - // ForumDropdownMenu(forumSharingViewModel, expanded, onClose, onLeaveForumClick) + dropdownMenu = { sharingViewModel, expanded, onClose, onLeaveForumClick -> + val privateGroupSharingViewModel = sharingViewModel as PrivateGroupSharingViewModel + PrivateGroupDropdownMenu(privateGroupSharingViewModel, expanded, onClose, onLeaveForumClick) } ) diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt index 593bac83d417b810fc612f682adaadc8e5b3479b..501c155c41de3e98b0d898839c156cf1ea14636a 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt @@ -39,7 +39,7 @@ import org.briarproject.briar.api.client.MessageTracker import org.briarproject.briar.api.privategroup.GroupMessageFactory import org.briarproject.briar.api.privategroup.JoinMessageHeader import org.briarproject.briar.api.privategroup.PrivateGroupManager -import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel +import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupSharingViewModel import org.briarproject.briar.desktop.threadedgroup.conversation.ThreadedConversationViewModel import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.threading.UiExecutor @@ -47,7 +47,7 @@ import java.lang.Long.max import javax.inject.Inject class PrivateGroupConversationViewModel @Inject constructor( - forumSharingViewModel: ForumSharingViewModel, + sharingViewModel: PrivateGroupSharingViewModel, private val privateGroupManager: PrivateGroupManager, private val privateGroupMessageFactory: GroupMessageFactory, private val identityManager: IdentityManager, @@ -58,7 +58,7 @@ class PrivateGroupConversationViewModel @Inject constructor( db: TransactionManager, eventBus: EventBus, ) : ThreadedConversationViewModel( - forumSharingViewModel, + sharingViewModel, briarExecutors, lifecycleManager, db, 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 new file mode 100644 index 0000000000000000000000000000000000000000..571b132e8f40a1305548cb274501e518e0924e2b --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupDropdownMenu.kt @@ -0,0 +1,84 @@ +/* + * Briar Desktop + * Copyright (C) 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.conversation + +import androidx.compose.material.DropdownMenu +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.PrivateGroupSharingViewModel +import org.briarproject.briar.desktop.ui.getInfoDrawerHandler +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n + +@Composable +fun PrivateGroupDropdownMenu( + privateGroupSharingViewModel: PrivateGroupSharingViewModel, + expanded: Boolean, + onClose: () -> Unit, + onLeaveForumClick: () -> Unit, +) = DropdownMenu( + expanded = expanded, + onDismissRequest = onClose, +) { + val infoDrawerHandler = getInfoDrawerHandler() +// DropdownMenuItem( +// onClick = { +// onClose() +// infoDrawerHandler.open { +// ForumSharingActionDrawerContent( +// close = infoDrawerHandler::close, +// viewModel = forumSharingViewModel, +// ) +// } +// } +// ) { +// Text( +// i18n("forum.sharing.action.title"), +// style = MaterialTheme.typography.body2, +// ) +// } +// DropdownMenuItem( +// onClick = { +// onClose() +// infoDrawerHandler.open { +// ForumSharingStatusDrawerContent( +// close = infoDrawerHandler::close, +// viewModel = forumSharingViewModel, +// ) +// } +// } +// ) { +// Text( +// i18n("forum.sharing.status.title"), +// 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/PrivateGroupSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..46ea4eedda10ee21b14888a685655bafe50e406f --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt @@ -0,0 +1,184 @@ +/* + * 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.runtime.derivedStateOf +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.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.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.threadedgroup.sharing.ThreadedGroupSharingViewModel +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.viewmodel.asState +import org.briarproject.briar.desktop.viewmodel.update +import javax.inject.Inject + +class PrivateGroupSharingViewModel @Inject constructor( + private val forumSharingManager: ForumSharingManager, + contactManager: ContactManager, + authorManager: AuthorManager, + conversationManager: ConversationManager, + private val connectionRegistry: ConnectionRegistry, + briarExecutors: BriarExecutors, + lifecycleManager: LifecycleManager, + db: TransactionManager, + eventBus: EventBus, +) : ThreadedGroupSharingViewModel( + contactManager, + authorManager, + conversationManager, + connectionRegistry, + briarExecutors, + lifecycleManager, + db, + eventBus, +) { + + 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() } + + override fun onInit() { + super.onInit() + loadContacts() + } + + override fun reload() { + super.reload() + _shareableSelected.value = emptySet() + _sharingMessage.value = "" + } + + @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.takeUtf8(MAX_INVITATION_TEXT_LENGTH) + } + + @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) + } + txn.attach { reload() } + } + + @UiExecutor + override fun eventOccurred(e: Event) { + super.eventOccurred(e) + + val groupId = _groupId ?: return + 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 loadSharingStatus(txn: Transaction) { + val groupId = _groupId ?: return + 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) + } + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedConversationViewModel.kt index b14b6456b4f761b9a586e09bd2442017b008c91a..3e5122191d51d39fcdc4490504a93bac2fef2277 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedConversationViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedConversationViewModel.kt @@ -30,15 +30,15 @@ import org.briarproject.bramble.api.sync.MessageId import org.briarproject.briar.api.client.PostHeader import org.briarproject.briar.client.MessageTreeImpl import org.briarproject.briar.desktop.forum.ForumPostReadEvent -import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupItem +import org.briarproject.briar.desktop.threadedgroup.sharing.ThreadedGroupSharingViewModel import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.threading.UiExecutor import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel import org.briarproject.briar.desktop.viewmodel.asState abstract class ThreadedConversationViewModel( - val forumSharingViewModel: ForumSharingViewModel, + val sharingViewModel: ThreadedGroupSharingViewModel, briarExecutors: BriarExecutors, lifecycleManager: LifecycleManager, db: TransactionManager, @@ -61,18 +61,18 @@ abstract class ThreadedConversationViewModel( this.onThreadItemLocallyAdded = onThreadItemLocallyAdded _threadedGroupItem.value = threadedGroupItem _selectedThreadItem.value = null - forumSharingViewModel.setGroupId(threadedGroupItem.id) + sharingViewModel.setGroupId(threadedGroupItem.id) loadThreadItems(threadedGroupItem.id) } override fun onInit() { super.onInit() - forumSharingViewModel.onEnterComposition() + sharingViewModel.onEnterComposition() } override fun onCleared() { super.onCleared() - forumSharingViewModel.onExitComposition() + sharingViewModel.onExitComposition() } protected abstract fun loadThreadItems(txn: Transaction, groupId: GroupId): List<ThreadItem> diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationScreen.kt index 6f5422be11a6939e0339b1d395b745d6da05d56a..cc995cbf2899bbf553bf24fab67c97a46da75fe4 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationScreen.kt @@ -51,10 +51,10 @@ 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.forum.sharing.ForumSharingViewModel import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupCircle import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupItem import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupStrings +import org.briarproject.briar.desktop.threadedgroup.sharing.ThreadedGroupSharingViewModel import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @@ -71,7 +71,7 @@ fun ThreadedGroupConversationScreen( ThreadedGroupConversationHeader( strings = strings, threadedGroupItem = groupItem, - forumSharingViewModel = viewModel.forumSharingViewModel, + sharingViewModel = viewModel.sharingViewModel, onGroupDelete = viewModel::deleteGroup, dropdownMenu = dropdownMenu, ) @@ -100,7 +100,7 @@ fun ThreadedGroupConversationScreen( private fun ThreadedGroupConversationHeader( strings: ThreadedGroupStrings, threadedGroupItem: ThreadedGroupItem, - forumSharingViewModel: ForumSharingViewModel, + sharingViewModel: ThreadedGroupSharingViewModel, onGroupDelete: () -> Unit, dropdownMenu: ThreadedGroupDropdownMenu, ) { @@ -129,7 +129,7 @@ private fun ThreadedGroupConversationHeader( overflow = Ellipsis, style = MaterialTheme.typography.h2, ) - val sharingInfo = forumSharingViewModel.sharingInfo.value + val sharingInfo = sharingViewModel.sharingInfo.value Text( text = strings.sharedWith(sharingInfo.total, sharingInfo.online) ) @@ -142,7 +142,7 @@ private fun ThreadedGroupConversationHeader( modifier = Modifier.align(CenterVertically).padding(end = 16.dp), ) { dropdownMenu( - forumSharingViewModel, + sharingViewModel, menuState.value == MAIN, close ) { deleteGroupDialogVisible.value = true } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupDropdownMenu.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupDropdownMenu.kt index 0e567fc9f54fecdb1eff73c1a93cf7ffbfcb3eed..dc6909d9505934e72a080f532719be1e6502ebd2 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupDropdownMenu.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupDropdownMenu.kt @@ -19,10 +19,10 @@ package org.briarproject.briar.desktop.threadedgroup.conversation import androidx.compose.runtime.Composable -import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel +import org.briarproject.briar.desktop.threadedgroup.sharing.ThreadedGroupSharingViewModel typealias ThreadedGroupDropdownMenu = @Composable ( - forumSharingViewModel: ForumSharingViewModel, + sharingViewModel: ThreadedGroupSharingViewModel, expanded: Boolean, onClose: () -> Unit, onLeaveGroupClick: () -> Unit, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/sharing/ThreadedGroupSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/sharing/ThreadedGroupSharingViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..fe9f1379352e71bdbe3d7c18d2f72a1bddfebea1 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/sharing/ThreadedGroupSharingViewModel.kt @@ -0,0 +1,102 @@ +/* + * 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.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 +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.desktop.contact.ContactsViewModel +import org.briarproject.briar.desktop.threading.BriarExecutors +import org.briarproject.briar.desktop.threading.UiExecutor +import org.briarproject.briar.desktop.viewmodel.asState + +abstract class ThreadedGroupSharingViewModel( + contactManager: ContactManager, + authorManager: AuthorManager, + conversationManager: ConversationManager, + connectionRegistry: ConnectionRegistry, + briarExecutors: BriarExecutors, + lifecycleManager: LifecycleManager, + db: TransactionManager, + eventBus: EventBus, +) : ContactsViewModel( + contactManager, + authorManager, + conversationManager, + connectionRegistry, + briarExecutors, + lifecycleManager, + db, + eventBus, +) { + protected var _groupId: GroupId? = null + + protected val _sharingInfo = mutableStateOf(SharingInfo(0, 0)) + val sharingInfo = _sharingInfo.asState() + + override fun onInit() { + super.onInit() + loadContacts() + } + + @UiExecutor + fun setGroupId(groupId: GroupId) { + if (_groupId == groupId) return + _groupId = groupId + 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) + + 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/viewmodel/ViewModelModule.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt index 244409ecf8fe86d49cca889a12887ae8b32c6876..aee751faa6747bd25f232a1aa21ca6422da90383 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 @@ -32,6 +32,7 @@ import org.briarproject.briar.desktop.login.StartupViewModel import org.briarproject.briar.desktop.mailbox.MailboxViewModel import org.briarproject.briar.desktop.navigation.SidebarViewModel import org.briarproject.briar.desktop.privategroup.PrivateGroupListViewModel +import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupSharingViewModel import org.briarproject.briar.desktop.settings.SettingsViewModel import kotlin.reflect.KClass @@ -85,6 +86,11 @@ abstract class ViewModelModule { @ViewModelKey(ForumSharingViewModel::class) abstract fun bindForumSharingViewModel(forumSharingViewModel: ForumSharingViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(PrivateGroupSharingViewModel::class) + abstract fun bindPrivateGroupSharingViewModel(privateGroupSharingViewModel: PrivateGroupSharingViewModel): ViewModel + @Binds @IntoMap @ViewModelKey(MailboxViewModel::class)