diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt index a09a4d1ced8f13d8a4129b239ad7cb1b89fe71de..bd5e773b4ff4f47395c732417e232dc34b5ec0e6 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt @@ -165,7 +165,7 @@ private fun PreviewUtils.PreviewScope.mapErrors(name: String?): AddContactError? @Composable fun AddContactDialog( - viewModel: AddContactViewModel = viewModel(), + viewModel: AddContactViewModel, ) = AddContactDialog( viewModel::dismissDialog, viewModel.visible.value, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt index 2df8d312002f14035387dff51e6b5c464d8861d1..5e36f631adb9dec2c029de6d2bb47f6de448aca5 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt @@ -73,6 +73,7 @@ import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel import org.briarproject.briar.desktop.viewmodel.SingleStateEvent 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 import kotlin.concurrent.thread @@ -373,13 +374,13 @@ constructor( is ContactConnectedEvent -> { if (e.contactId == _contactId.value) { LOG.i { "Contact connected" } - _contactItem.value = _contactItem.value!!.updateIsConnected(true) + _contactItem.update { this?.updateIsConnected(true) } } } is ContactDisconnectedEvent -> { if (e.contactId == _contactId.value) { LOG.i { "Contact disconnected" } - _contactItem.value = _contactItem.value!!.updateIsConnected(false) + _contactItem.update { this?.updateIsConnected(false) } } } is ClientVersionUpdatedEvent -> { @@ -459,7 +460,7 @@ constructor( val newAlias = _newAlias.value.ifBlank { null } if (_contactId.value != null && contactItem.value != null) { contactManager.setContactAlias(_contactId.value!!, newAlias) - _contactItem.value = contactItem.value!!.updateAlias(newAlias) + _contactItem.update { this?.updateAlias(newAlias) } } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt index 67511a08d261cf4248df9a67f210356554ce04b8..42065c51fbebd05694032f75d8096e7f85b9de06 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt @@ -55,7 +55,7 @@ fun PrivateMessageScreen( viewModel: ContactListViewModel = viewModel(), addContactViewModel: AddContactViewModel = viewModel(), ) { - AddContactDialog() + AddContactDialog(addContactViewModel) ConfirmRemovePendingContactDialog( viewModel.removePendingContactDialogVisible.value, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt index b06a03988c938f318e393f05de5887b6c2f9f89a..14ea3742af0a748ea43145049802c15b80d8614d 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt @@ -52,7 +52,7 @@ import org.briarproject.briar.desktop.viewmodel.viewModel fun ForumSharingDrawerContent( groupId: GroupId, close: () -> Unit, - viewModel: ForumSharingViewModel = viewModel(), + viewModel: ForumSharingViewModel, ) = Column { LaunchedEffect(groupId) { viewModel.setGroupId(groupId) diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt index da70d650622e05877a6284800eb0c548f823fc3c..3ddff4f582030405bb029102e83e41c2b893f428 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt @@ -44,7 +44,9 @@ 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( @@ -67,7 +69,7 @@ class ForumSharingViewModel @Inject constructor( private lateinit var groupId: GroupId private val _currentlySharedWith = mutableStateListOf<ContactItem>() - val currentlySharedWith: List<ContactItem> = _currentlySharedWith + val currentlySharedWith = _currentlySharedWith.asList() private val _sharingInfo = mutableStateOf(SharingInfo(0, 0)) val sharingInfo = _sharingInfo.asState() @@ -96,28 +98,28 @@ class ForumSharingViewModel @Inject constructor( ) txn.attach { _currentlySharedWith.add(item) - _sharingInfo.value = _sharingInfo.value.addContact(connected) + _sharingInfo.update { addContact(connected) } } } e is ContactLeftShareableEvent && e.groupId == groupId -> { _currentlySharedWith.removeFirst { it.idWrapper.contactId == e.contactId } val connected = connectionRegistry.isConnected(e.contactId) - _sharingInfo.value = _sharingInfo.value.removeContact(connected) + _sharingInfo.update { removeContact(connected) } } e is ContactConnectedEvent -> { val isMember = _currentlySharedWith.replaceFirst({ it.idWrapper.contactId == e.contactId }) { it.updateIsConnected(true) } - if (isMember) _sharingInfo.value = _sharingInfo.value.updateContactConnected(true) + if (isMember) _sharingInfo.update { updateContactConnected(true) } } e is ContactDisconnectedEvent -> { val isMember = _currentlySharedWith.replaceFirst({ it.idWrapper.contactId == e.contactId }) { it.updateIsConnected(false) } - if (isMember) _sharingInfo.value = _sharingInfo.value.updateContactConnected(false) + if (isMember) _sharingInfo.update { updateContactConnected(false) } } } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt index 629bb298f4bf429f8a27059a98bff3d7b709fa47..507d80a609dffa08a3cfe1bc43d8638b43cea030 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt @@ -83,13 +83,13 @@ class ForumViewModel @Inject constructor( // since the threadViewModel is tightly coupled to the ForumViewModel // and not injected using the usual `viewModel()` approach, // we have to manually call the functions for (de)initialization - threadViewModel.onInit() + threadViewModel.onEnterComposition() loadGroups() } override fun onCleared() { super.onCleared() - threadViewModel.onCleared() + threadViewModel.onExitComposition() } override fun eventOccurred(e: Event) { 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 160a9c7be613bc436d4d6b95bd1af07f4a0a371b..b8a8adbb4c768c3c7027e6845c905c5f96ed877b 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 @@ -43,7 +43,6 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.BottomCenter @@ -59,7 +58,6 @@ import org.briarproject.briar.desktop.ui.HorizontalDivider import org.briarproject.briar.desktop.ui.getInfoDrawerHandler import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF -import org.briarproject.briar.desktop.viewmodel.viewModel @Composable fun GroupConversationScreen( @@ -67,7 +65,7 @@ fun GroupConversationScreen( ) { Scaffold( topBar = { - GroupConversationHeader(viewModel.groupItem) { + GroupConversationHeader(viewModel.groupItem, viewModel.forumSharingViewModel) { viewModel.deleteGroup(viewModel.groupItem) } }, @@ -92,14 +90,9 @@ fun GroupConversationScreen( @Composable private fun GroupConversationHeader( groupItem: GroupItem, - viewModel: ForumSharingViewModel = viewModel(), - // todo: using the same viewModel twice could lead to problems when one of the occurrences goes out of scope - // -> viewModel.onCleared() will be called and it will, e.g., stop listening to events + forumSharingViewModel: ForumSharingViewModel, onGroupDelete: () -> Unit, ) { - LaunchedEffect(groupItem) { - viewModel.setGroupId(groupItem.id) - } val deleteGroupDialogVisible = remember { mutableStateOf(false) } val menuState = remember { mutableStateOf(CLOSED) } val close = { menuState.value = CLOSED } @@ -126,7 +119,7 @@ private fun GroupConversationHeader( overflow = Ellipsis, style = MaterialTheme.typography.h2, ) - val sharingInfo = viewModel.sharingInfo.value + val sharingInfo = forumSharingViewModel.sharingInfo.value Text( text = i18nF("forum.sharing.status.with", sharingInfo.total, sharingInfo.online) ) @@ -149,6 +142,7 @@ private fun GroupConversationHeader( ForumSharingDrawerContent( groupId = groupItem.id, 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 4b62526064823cbe621aa0977e878089fefb69b1..3351fa255a49ae5333b45d0bd201e27b4a1ccf98 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 @@ -48,6 +48,7 @@ import java.lang.Long.max import javax.inject.Inject class ThreadedConversationViewModel @Inject constructor( + val forumSharingViewModel: ForumSharingViewModel, private val forumManager: ForumManager, private val identityManager: IdentityManager, private val clock: Clock, @@ -78,6 +79,7 @@ class ThreadedConversationViewModel @Inject constructor( this.groupItem = groupItem this.onPostAdded = onPostAdded _selectedPost.value = null + forumSharingViewModel.setGroupId(groupItem.id) loadPosts(groupItem.id) } @@ -91,6 +93,16 @@ class ThreadedConversationViewModel @Inject constructor( } } + override fun onInit() { + super.onInit() + forumSharingViewModel.onEnterComposition() + } + + override fun onCleared() { + super.onCleared() + forumSharingViewModel.onExitComposition() + } + private fun loadPosts(groupId: GroupId) { _posts.value = Loading runOnDbThreadWithTransaction(true) { txn -> diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt index 0c10d49c97fac3e6d99d67c100b3571c67b54e0c..223e98e9ab737db652fe8dd3a5f615267411fa33 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt @@ -30,7 +30,7 @@ class SidebarViewModel @Inject constructor( private val identityManager: IdentityManager, -) : ViewModel { +) : ViewModel() { override fun onInit() { loadAccountInfo() diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt index f6ebda2998632b87a9e2af78db6134dd2569526c..96d87757715f31a317772cb630c82ea567462cfb 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt @@ -72,13 +72,14 @@ inline fun <T, reified U : T> MutableList<T>.replaceFirst(predicate: (U) -> Bool return false } -inline fun <T, reified U : T> MutableList<T>.removeFirst(predicate: (U) -> Boolean) { +inline fun <T, reified U : T> MutableList<T>.removeFirst(predicate: (U) -> Boolean): Boolean { val li = listIterator() while (li.hasNext()) { val n = li.next() if (n is U && predicate(n)) { li.remove() - break + return true } } + return false } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt index aab516b5f773b7fc1f2b4604be5fc17044ced4ea..be5cb5a81a0d32671ea33dca15d53b57001e2cfd 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt @@ -59,6 +59,8 @@ inline fun <reified VM : ViewModel> viewModel( * It will be automatically initialized as soon as the calling screen is composed * for the first time, and cleared when it goes out of scope. * + * **Therefore, it is not allowed to call [viewModel] with the same type from within multiple Composables.** + * * @param modelClass The class of the [ViewModel] to create an instance of it if it is not * present. * @param viewModelProvider The scope that the created [ViewModel] should be associated with. @@ -76,10 +78,10 @@ fun <VM : ViewModel> viewModel( val viewModel = viewModelProvider.get(modelClass, key) DisposableEffect(key) { - viewModel.onInit() + viewModel.onEnterComposition() onDispose { - viewModel.onCleared() + viewModel.onExitComposition() } } @@ -95,3 +97,10 @@ fun <T> MutableState<T>.asState(): State<T> = this * Returns this [SnapshotStateList] as an immutable [List]. */ fun <T> SnapshotStateList<T>.asList(): List<T> = this + +/** + * Update the [MutableState] with the given [transformation] on its value. + */ +inline fun <T> MutableState<T>.update(transformation: T.() -> T) { + value = value.run(transformation) +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt index e3b97b9c3178eee70b8657db20de4559d8a4e907..a67eb885d68cfab93e5ca4edd636a8fc5420f901 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt @@ -28,7 +28,7 @@ abstract class DbViewModel( private val briarExecutors: BriarExecutors, private val lifecycleManager: LifecycleManager, private val db: TransactionManager, -) : ViewModel { +) : ViewModel() { /** * Waits for the DB to open and runs the given [task] on the [DatabaseExecutor]. diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt index c367cc24fe5d88aa717ee4ea1260ef4a5736e903..17af4811fa33cb65d8d45bafb8ded7f9a063bf69 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt @@ -18,17 +18,55 @@ package org.briarproject.briar.desktop.viewmodel -interface ViewModel { +abstract class ViewModel { + + private var inComposition = false + + /** + * Called to initialize the [ViewModel] as soon as it is first used + * inside a Composable function. + * + * If you want to specify initialization code, overwrite [onInit] instead. + */ + fun onEnterComposition() { + if (inComposition) + throw RuntimeException("Injecting the same instance of ${this::class.simpleName} in different Composables is not permitted.") + inComposition = true + onInit() + } + + /** + * Called to clear the [ViewModel] as soon as the calling + * Composable function goes out of scope. + * + * If you want to specify de-initialization code, overwrite [onCleared] instead. + */ + fun onExitComposition() { + if (!inComposition) + throw RuntimeException("Wrong use of ViewModel.") + inComposition = false + onCleared() + } /** * Called to initialize the [ViewModel] as soon as it is first used * inside a Composable function. + * + * This function can be overridden in child classes, + * but implementations should always call `super.onInit()` first. + * + * Apart from that, **do not call this function manually anywhere.** */ - fun onInit() {} + open fun onInit() {} /** * Called to clear the [ViewModel] as soon as the calling * Composable function goes out of scope. + * + * This function can be overridden in child classes, + * but implementations should always call `super.onInit()` first. + * + * Apart from that, **do not call this function manually anywhere.** */ - fun onCleared() {} + open fun onCleared() {} }