diff --git a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt index f73704ab017af6361d83e63f6975e198cbf254e1..4bd50fd4ca961fa27fae2857d1f4d115c9cbee53 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt @@ -29,6 +29,7 @@ import org.briarproject.bramble.util.OsUtils.isLinux import org.briarproject.bramble.util.OsUtils.isMac import org.briarproject.briar.desktop.ui.BriarUi import org.briarproject.briar.desktop.ui.BriarUiImpl +import org.briarproject.briar.desktop.viewmodel.ViewModelModule import java.io.File import java.nio.file.Path import java.util.Collections.emptyList @@ -46,7 +47,8 @@ import javax.inject.Singleton DesktopSecureRandomModule::class, JavaNetworkModule::class, JavaSystemModule::class, - SocksModule::class + SocksModule::class, + ViewModelModule::class, ] ) internal class DesktopModule( diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt index f43736a975bd38b7427f2e45fc0ba9782d447131..a0732821932654c0afdad88ad942abafb77c1d13 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt @@ -1,9 +1,9 @@ package org.briarproject.briar.desktop.contact import androidx.compose.runtime.Composable +import org.briarproject.bramble.api.contact.Contact import org.briarproject.briar.desktop.contact.ContactInfoDrawerState.MakeIntro import org.briarproject.briar.desktop.introduction.ContactDrawerMakeIntro -import org.briarproject.briar.desktop.introduction.IntroductionViewModel // Right drawer state enum class ContactInfoDrawerState { @@ -14,11 +14,11 @@ enum class ContactInfoDrawerState { @Composable fun ContactInfoDrawer( - introductionViewModel: IntroductionViewModel, + contact: Contact, setInfoDrawer: (Boolean) -> Unit, drawerState: ContactInfoDrawerState ) { when (drawerState) { - MakeIntro -> ContactDrawerMakeIntro(introductionViewModel, setInfoDrawer) + MakeIntro -> ContactDrawerMakeIntro(contact, setInfoDrawer) } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt index 06913697cf7cf93bbf2f75173a4bd20b0883fbcc..16b1f0a3bc25f02e4b428488ca36781b67e1af3a 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt @@ -16,19 +16,22 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import org.briarproject.bramble.api.contact.ContactId import org.briarproject.briar.desktop.contact.add.remote.AddContactDialog -import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel import org.briarproject.briar.desktop.theme.surfaceVariant import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE @Composable fun ContactList( - viewModel: ContactListViewModel, - addContactViewModel: AddContactViewModel, + contactList: List<ContactItem>, + isSelected: (ContactId) -> Boolean, + selectContact: (ContactId) -> Unit, + filterBy: String, + setFilterBy: (String) -> Unit, ) { var isContactDialogVisible by remember { mutableStateOf(false) } - if (isContactDialogVisible) AddContactDialog(addContactViewModel) { isContactDialogVisible = false } + if (isContactDialogVisible) AddContactDialog(onClose = { isContactDialogVisible = false }) Scaffold( modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH), backgroundColor = MaterialTheme.colors.surfaceVariant, @@ -37,19 +40,19 @@ fun ContactList( modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp), ) { SearchTextField( - viewModel.filterBy.value, - onValueChange = viewModel::setFilterBy, + filterBy, + onValueChange = setFilterBy, onContactAdd = { isContactDialogVisible = true } ) } }, content = { LazyColumn { - items(viewModel.contactList) { contactItem -> + items(contactList) { contactItem -> ContactCard( contactItem, - { viewModel.selectContact(contactItem.contact.id) }, - viewModel.isSelected(contactItem.contact.id) + { selectContact(contactItem.contact.id) }, + isSelected(contactItem.contact.id) ) } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt index 30e3cc04df2c931dad9dfab31341e701d982c435..0617d92f2fa23e1431699f51bfa6a30a337ff1cb 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt @@ -22,15 +22,21 @@ constructor( conversationManager: ConversationManager, connectionRegistry: ConnectionRegistry, eventBus: EventBus, -) : ContactsViewModel(contactManager, conversationManager, connectionRegistry) { +) : ContactsViewModel(contactManager, conversationManager, connectionRegistry, eventBus) { companion object { private val LOG = KotlinLogging.logger {} } - init { - // todo: where/when to remove listener again? - eventBus.addListener(this) + override fun onInit() { + super.onInit() + loadContacts() + } + + override fun onCleared() { + super.onCleared() + // todo: also reset filterBy? + _selectedContactId.value = null } private val _filterBy = mutableStateOf("") diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt index 4bb2e4a7a4584626339d769f162c241aa2fb0b03..157e028f680982d1a629373808fd3768355971d0 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt @@ -9,18 +9,20 @@ 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.event.Event -import org.briarproject.bramble.api.event.EventListener +import org.briarproject.bramble.api.event.EventBus 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.desktop.utils.removeFirst import org.briarproject.briar.desktop.utils.replaceFirst +import org.briarproject.briar.desktop.viewmodel.BriarEventListenerViewModel abstract class ContactsViewModel( protected val contactManager: ContactManager, private val conversationManager: ConversationManager, - private val connectionRegistry: ConnectionRegistry -) : EventListener { + private val connectionRegistry: ConnectionRegistry, + eventBus: EventBus, +) : BriarEventListenerViewModel(eventBus) { companion object { private val LOG = KotlinLogging.logger {} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt index db2f6dc5062a28c140c4fd422398423a66bdf596..c66906e7ae9096d63fbd4613fb542979ef4e8a06 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt @@ -15,19 +15,18 @@ import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.TextField 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 androidx.compose.ui.unit.sp +import org.briarproject.briar.desktop.viewmodel.viewModel @OptIn(ExperimentalMaterialApi::class) @Composable -fun AddContactDialog(viewModel: AddContactViewModel, onClose: () -> Unit) { - LaunchedEffect("fetchHandshake") { - // todo: should instead be done automatically as soon as DB is loaded -> in view model - viewModel.fetchHandshakeLink() - } +fun AddContactDialog( + onClose: () -> Unit, + viewModel: AddContactViewModel = viewModel(), +) { AlertDialog( onDismissRequest = onClose, text = { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt index a429601f5c37614000419c9cd1e39fb8e1312f5a..7d9fe3fcd1ec5bafea7a07ea067189c5d1070d8f 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt @@ -9,6 +9,7 @@ import org.briarproject.bramble.api.db.ContactExistsException import org.briarproject.bramble.api.db.PendingContactExistsException import org.briarproject.bramble.api.identity.AuthorConstants import org.briarproject.bramble.util.StringUtils +import org.briarproject.briar.desktop.viewmodel.ViewModel import java.security.GeneralSecurityException import javax.inject.Inject @@ -16,7 +17,11 @@ class AddContactViewModel @Inject constructor( private val contactManager: ContactManager, -) { +) : ViewModel { + + override fun onInit() { + fetchHandshakeLink() + } private val _alias = mutableStateOf("") private val _remoteHandshakeLink = mutableStateOf("") diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt similarity index 83% rename from src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt index dee4283f89c6ef0877e195600cb87168b5b0abda..97604a38d77f6725c8ae7de446b663cca6a40a2a 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt @@ -31,23 +31,22 @@ import androidx.compose.ui.unit.dp import org.briarproject.bramble.api.contact.ContactId import org.briarproject.briar.desktop.contact.ContactInfoDrawer import org.briarproject.briar.desktop.contact.ContactInfoDrawerState -import org.briarproject.briar.desktop.introduction.IntroductionViewModel import org.briarproject.briar.desktop.navigation.SIDEBAR_WIDTH import org.briarproject.briar.desktop.theme.surfaceVariant import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH import org.briarproject.briar.desktop.ui.Loader +import org.briarproject.briar.desktop.viewmodel.viewModel @Composable -fun Conversation( +fun ConversationScreen( contactId: ContactId, - conversationViewModel: ConversationViewModel, - introductionViewModel: IntroductionViewModel, + viewModel: ConversationViewModel = viewModel(), ) { LaunchedEffect(contactId) { - conversationViewModel.setContactId(contactId) + viewModel.setContactId(contactId) } - val contactItem = conversationViewModel.contactItem.value + val contactItem = viewModel.contactItem.value if (contactItem == null) { Loader() @@ -63,10 +62,6 @@ fun Conversation( ConversationHeader( contactItem, onMakeIntroduction = { - introductionViewModel.apply { - setFirstContact(contactItem.contact) - loadContacts() - } setInfoDrawer(true) } ) @@ -79,7 +74,7 @@ fun Conversation( contentPadding = PaddingValues(8.dp), modifier = Modifier.padding(padding).fillMaxHeight() ) { - items(conversationViewModel.messages) { m -> + items(viewModel.messages) { m -> if (m is ConversationMessageItem) TextBubble(m) } @@ -87,9 +82,9 @@ fun Conversation( }, bottomBar = { ConversationInput( - conversationViewModel.newMessage.value, - conversationViewModel::setNewMessage, - conversationViewModel::sendMessage + viewModel.newMessage.value, + viewModel::setNewMessage, + viewModel::sendMessage ) }, ) @@ -113,7 +108,7 @@ fun Conversation( RoundedCornerShape(topStart = 10.dp, bottomStart = 10.dp) ) ) { - ContactInfoDrawer(introductionViewModel, setInfoDrawer, contactDrawerState) + ContactInfoDrawer(contactItem.contact, setInfoDrawer, contactDrawerState) } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt index a1c864d2019ed1eb56ac53d6262e2c96a4d6e25d..97d7a96a76dda3289434f992b35b3030a4c926fa 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt @@ -13,7 +13,6 @@ import org.briarproject.bramble.api.db.DbException import org.briarproject.bramble.api.db.NoSuchContactException 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.plugin.event.ContactConnectedEvent import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent import org.briarproject.bramble.api.sync.MessageId @@ -32,6 +31,7 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader import org.briarproject.briar.desktop.contact.ContactItem import org.briarproject.briar.desktop.utils.KLoggerUtils.logDuration import org.briarproject.briar.desktop.utils.replaceIf +import org.briarproject.briar.desktop.viewmodel.BriarEventListenerViewModel import java.util.Date import javax.inject.Inject @@ -42,19 +42,14 @@ constructor( private val contactManager: ContactManager, private val conversationManager: ConversationManager, private val messagingManager: MessagingManager, - private val eventBus: EventBus, private val privateMessageFactory: PrivateMessageFactory, -) : EventListener { + private val eventBus: EventBus, +) : BriarEventListenerViewModel(eventBus) { companion object { private val LOG = KotlinLogging.logger {} } - init { - // todo: where/when to remove listener again? - eventBus.addListener(this) - } - private val _contactId = mutableStateOf<ContactId?>(null) private val _contactItem = mutableStateOf<ContactItem?>(null) private val _messages = mutableStateListOf<ConversationItem>() diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt similarity index 59% rename from src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt index 655237c74c32986887dda65b241a95b822a5ad6a..3fc3d795352aa9be3c7f80294c4a992c1ddefcad 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt @@ -8,25 +8,27 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import org.briarproject.briar.desktop.contact.ContactList import org.briarproject.briar.desktop.contact.ContactListViewModel -import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel -import org.briarproject.briar.desktop.introduction.IntroductionViewModel import org.briarproject.briar.desktop.ui.UiPlaceholder import org.briarproject.briar.desktop.ui.VerticalDivider +import org.briarproject.briar.desktop.viewmodel.viewModel @Composable -fun PrivateMessageView( - contactListViewModel: ContactListViewModel, - conversationViewModel: ConversationViewModel, - addContactViewModel: AddContactViewModel, - introductionViewModel: IntroductionViewModel, +fun PrivateMessageScreen( + viewModel: ContactListViewModel = viewModel(), ) { Row(modifier = Modifier.fillMaxWidth()) { - ContactList(contactListViewModel, addContactViewModel) + ContactList( + viewModel.contactList, + viewModel::isSelected, + viewModel::selectContact, + viewModel.filterBy.value, + viewModel::setFilterBy + ) VerticalDivider() Column(modifier = Modifier.weight(1f).fillMaxHeight()) { - val id = contactListViewModel.selectedContactId.value + val id = viewModel.selectedContactId.value if (id != null) { - Conversation(id, conversationViewModel, introductionViewModel) + ConversationScreen(id) } else { UiPlaceholder() } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt index 8716c44ef85401d10401dcdc55c33f028588690e..c10f6f4519bc0922cf8f5f9065809440158a6e67 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt @@ -20,24 +20,31 @@ import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.briarproject.bramble.api.contact.Contact import org.briarproject.briar.desktop.contact.ContactCard import org.briarproject.briar.desktop.contact.ProfileCircle import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF +import org.briarproject.briar.desktop.viewmodel.viewModel import java.util.Locale @Composable fun ContactDrawerMakeIntro( - viewModel: IntroductionViewModel, - setInfoDrawer: (Boolean) -> Unit + contact: Contact, + setInfoDrawer: (Boolean) -> Unit, + viewModel: IntroductionViewModel = viewModel(), ) { + LaunchedEffect(contact) { + viewModel.setFirstContact(contact) + } if (!viewModel.secondScreen.value) { Surface { Column { @@ -49,7 +56,7 @@ fun ContactDrawerMakeIntro( Icon(Icons.Filled.Close, i18n("access.introduction.close")) } Text( - text = i18nF("introduction.title_first", viewModel.firstContact.value!!.author.name), + text = i18nF("introduction.title_first", contact.author.name), fontSize = 16.sp, modifier = Modifier.align(Alignment.CenterVertically) ) diff --git a/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt index 6c9481af60ddd72cd810a5fe147ebe1e9ddcddbb..e8b2e63bfdfba6959305f49b6c85c6b3f1830001 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt @@ -17,12 +17,7 @@ constructor( conversationManager: ConversationManager, connectionRegistry: ConnectionRegistry, eventBus: EventBus, -) : ContactsViewModel(contactManager, conversationManager, connectionRegistry) { - - init { - // todo: where/when to remove listener again? - eventBus.addListener(this) - } +) : ContactsViewModel(contactManager, conversationManager, connectionRegistry, eventBus) { private val _firstContact = mutableStateOf<Contact?>(null) private val _secondContact = mutableStateOf<Contact?>(null) diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt similarity index 90% rename from src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt rename to src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt index bfc6ff0d8360769bfcc78f6b71961fc0b5758333..c3f072c1fdcef4968e7b86108a9cfd77136635b4 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt @@ -16,7 +16,7 @@ import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -35,25 +35,23 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.viewmodel.viewModel // TODO: Error handling @OptIn(ExperimentalComposeUiApi::class) @Composable -fun Login( - viewModel: LoginViewModel, - modifier: Modifier = Modifier, - onSignedIn: () -> Unit +fun LoginScreen( + onSignedIn: () -> Unit, + viewModel: LoginViewModel = viewModel(), ) { val signIn = { - viewModel.signIn { - onSignedIn() - } + viewModel.signIn(onSignedIn) } val initialFocusRequester = remember { FocusRequester() } Surface { Column( - modifier = modifier.padding(16.dp).fillMaxSize(), + modifier = Modifier.padding(16.dp).fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -81,9 +79,8 @@ fun Login( Text(i18n("login.login")) } - DisposableEffect(Unit) { + LaunchedEffect(true) { initialFocusRequester.requestFocus() - onDispose { } } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt index a86e37f42534991545ca1340499d7ceb96547855..85d939c109aba3be7e47078f0e626dbb3ea56605 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import org.briarproject.bramble.api.account.AccountManager import org.briarproject.bramble.api.crypto.DecryptionException import org.briarproject.bramble.api.lifecycle.LifecycleManager +import org.briarproject.briar.desktop.viewmodel.ViewModel import javax.inject.Inject class LoginViewModel @@ -12,7 +13,7 @@ class LoginViewModel constructor( private val accountManager: AccountManager, private val lifecycleManager: LifecycleManager, -) { +) : ViewModel { private val _password = mutableStateOf("") diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt similarity index 93% rename from src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt rename to src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt index e90a0d6c1f378e23c39fa21aeb9db3b165c96c1d..7e3251ad844ba725c5079638d39c313117f587c4 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt @@ -34,27 +34,23 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.viewmodel.viewModel import java.util.Locale // TODO: Error handling and password strength @OptIn(ExperimentalComposeUiApi::class) @Composable -fun Registration( - viewModel: RegistrationViewModel, - modifier: Modifier = Modifier, - onSignedUp: () -> Unit +fun RegistrationScreen( + onSignedUp: () -> Unit, + viewModel: RegistrationViewModel = viewModel(), ) { - val signUp = { - viewModel.signUp { - onSignedUp() - } - } + val signUp = { viewModel.signUp(onSignedUp) } val initialFocusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current Surface { Column( - modifier = modifier.padding(16.dp).fillMaxSize(), + modifier = Modifier.padding(16.dp).fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt index 6a133b40d2e7bf54030140735869042633e57227..b36dffd9fdf63a1cb7504a37324229963a50b090 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import org.briarproject.bramble.api.account.AccountManager import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator import org.briarproject.bramble.api.lifecycle.LifecycleManager +import org.briarproject.briar.desktop.viewmodel.ViewModel import javax.inject.Inject class RegistrationViewModel @@ -13,7 +14,7 @@ constructor( private val accountManager: AccountManager, private val lifecycleManager: LifecycleManager, private val passwordStrengthEstimator: PasswordStrengthEstimator, -) { +) : ViewModel { private var isSafeEnough = mutableStateOf(false) private val _username = mutableStateOf("") diff --git a/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt b/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt index 023bb259c15940254515a723e39a59f4231f5958..6bdbae821bf38e4260ae063e607d5187eac4d5e4 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt @@ -19,11 +19,11 @@ import androidx.compose.material.icons.filled.Logout import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.WifiTethering import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp +import org.briarproject.bramble.api.identity.LocalAuthor import org.briarproject.briar.desktop.contact.ProfileCircle import org.briarproject.briar.desktop.theme.sidebarSurface import org.briarproject.briar.desktop.ui.UiMode @@ -32,10 +32,17 @@ val SIDEBAR_WIDTH = 56.dp @Composable fun BriarSidebar( - viewModel: SidebarViewModel, + account: LocalAuthor?, + uiMode: UiMode, + setUiMode: (UiMode) -> Unit, ) { - LaunchedEffect(true) { - viewModel.loadAccountInfo() + val displayButton = @Composable { selectedMode: UiMode, mode: UiMode, icon: ImageVector -> + BriarSidebarButton( + selectedMode == mode, + { setUiMode(mode) }, + icon, + mode.toString() + ) } Surface(modifier = Modifier.width(SIDEBAR_WIDTH).fillMaxHeight(), color = MaterialTheme.colors.sidebarSurface) { @@ -45,39 +52,29 @@ fun BriarSidebar( modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 5.dp, bottom = 4.dp), onClick = {} ) { - viewModel.account.value?.let { ProfileCircle(size = 45.dp, it.id.bytes) } + account?.let { ProfileCircle(size = 45.dp, it.id.bytes) } } for ( - (uiMode, icon) in listOf( + (mode, icon) in listOf( Pair(UiMode.CONTACTS, Icons.Filled.Contacts), Pair(UiMode.GROUPS, Icons.Filled.Group), Pair(UiMode.FORUMS, Icons.Filled.Forum), Pair(UiMode.BLOGS, Icons.Filled.ChromeReaderMode), ) ) { - BriarSidebarButton( - viewModel.uiMode.value == uiMode, - { viewModel.setUiMode(uiMode) }, - icon, - uiMode.toString() - ) + displayButton(uiMode, mode, icon) } } Column(verticalArrangement = Arrangement.Bottom) { for ( - (uiMode, icon) in listOf( + (mode, icon) in listOf( Pair(UiMode.TRANSPORTS, Icons.Filled.WifiTethering), Pair(UiMode.SETTINGS, Icons.Filled.Settings), Pair(UiMode.SIGNOUT, Icons.Filled.Logout), ) ) { - BriarSidebarButton( - viewModel.uiMode.value == uiMode, - { viewModel.setUiMode(uiMode) }, - icon, - uiMode.toString() - ) + displayButton(uiMode, mode, icon) } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt index e7b560ce95b67ad6b0cb615635e7ba0362de4a86..414e17709b0a5d4b06cc2806901569b3cd84854e 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt @@ -5,13 +5,18 @@ import androidx.compose.runtime.mutableStateOf import org.briarproject.bramble.api.identity.IdentityManager import org.briarproject.bramble.api.identity.LocalAuthor import org.briarproject.briar.desktop.ui.UiMode +import org.briarproject.briar.desktop.viewmodel.ViewModel import javax.inject.Inject class SidebarViewModel @Inject constructor( private val identityManager: IdentityManager, -) { +) : ViewModel { + + override fun onInit() { + loadAccountInfo() + } private var _uiMode = mutableStateOf(UiMode.CONTACTS) private var _account = mutableStateOf<LocalAuthor?>(null) diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index 7d0005487abee625fd4a7c204a2d99cdf0f3bf3d..3d9ad8ddfc789fd8e3697f042804af4cb1fabfaa 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -1,26 +1,22 @@ package org.briarproject.briar.desktop.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.res.painterResource import androidx.compose.ui.window.Window import org.briarproject.bramble.api.account.AccountManager import org.briarproject.bramble.api.lifecycle.LifecycleManager import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING -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.introduction.IntroductionViewModel -import org.briarproject.briar.desktop.login.Login -import org.briarproject.briar.desktop.login.LoginViewModel -import org.briarproject.briar.desktop.login.Registration -import org.briarproject.briar.desktop.login.RegistrationViewModel -import org.briarproject.briar.desktop.navigation.SidebarViewModel +import org.briarproject.briar.desktop.login.LoginScreen +import org.briarproject.briar.desktop.login.RegistrationScreen import org.briarproject.briar.desktop.theme.BriarTheme import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.viewmodel.ViewModelProvider import java.awt.Dimension import javax.annotation.concurrent.Immutable import javax.inject.Inject @@ -40,20 +36,16 @@ interface BriarUi { fun stop() } +val LocalViewModelProvider = staticCompositionLocalOf<ViewModelProvider?> { null } + @Immutable @Singleton internal class BriarUiImpl @Inject constructor( - private val registrationViewModel: RegistrationViewModel, - private val loginViewModel: LoginViewModel, - private val contactListViewModel: ContactListViewModel, - private val conversationViewModel: ConversationViewModel, - private val addContactViewModel: AddContactViewModel, - private val introductionViewModel: IntroductionViewModel, - private val sidebarViewModel: SidebarViewModel, private val accountManager: AccountManager, private val lifecycleManager: LifecycleManager, + private val viewModelProvider: ViewModelProvider, ) : BriarUi { override fun stop() { @@ -73,7 +65,6 @@ constructor( if (accountManager.hasDatabaseKey()) { // this should only happen during testing when we launch the main UI directly // without a need to enter the password. - contactListViewModel.loadContacts() Screen.MAIN } else if (accountManager.accountExists()) { Screen.LOGIN @@ -88,28 +79,24 @@ constructor( icon = painterResource("images/logo_circle.svg") ) { window.minimumSize = Dimension(800, 600) - BriarTheme(isDarkTheme = isDark) { - when (screenState) { - Screen.REGISTRATION -> - Registration(registrationViewModel) { - contactListViewModel.loadContacts() - screenState = Screen.MAIN - } - Screen.LOGIN -> - Login(loginViewModel) { - contactListViewModel.loadContacts() - screenState = Screen.MAIN - } - else -> - MainScreen( - contactListViewModel, - conversationViewModel, - addContactViewModel, - introductionViewModel, - sidebarViewModel, - isDark, - setDark - ) + CompositionLocalProvider(LocalViewModelProvider provides viewModelProvider) { + BriarTheme(isDarkTheme = isDark) { + when (screenState) { + Screen.REGISTRATION -> + RegistrationScreen( + onSignedUp = { + screenState = Screen.MAIN + } + ) + Screen.LOGIN -> + LoginScreen( + onSignedIn = { + screenState = Screen.MAIN + } + ) + else -> + MainScreen(isDark, setDark) + } } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt index e6f11c87dc9ac664921810bb8c125d39f8ab4339..c636fa7384a9510090c94353db27b744fc954ce5 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt @@ -2,14 +2,11 @@ package org.briarproject.briar.desktop.ui import androidx.compose.foundation.layout.Row import androidx.compose.runtime.Composable -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.conversation.PrivateMessageView -import org.briarproject.briar.desktop.introduction.IntroductionViewModel +import org.briarproject.briar.desktop.conversation.PrivateMessageScreen import org.briarproject.briar.desktop.navigation.BriarSidebar import org.briarproject.briar.desktop.navigation.SidebarViewModel import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView +import org.briarproject.briar.desktop.viewmodel.viewModel /* * This is the root of the tree, all state is held here and passed down to stateless composables, which render the UI @@ -18,24 +15,19 @@ import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView */ @Composable fun MainScreen( - contactListViewModel: ContactListViewModel, - conversationViewModel: ConversationViewModel, - addContactViewModel: AddContactViewModel, - introductionViewModel: IntroductionViewModel, - sidebarViewModel: SidebarViewModel, isDark: Boolean, - setDark: (Boolean) -> Unit + setDark: (Boolean) -> Unit, + viewModel: SidebarViewModel = viewModel(), ) { Row { - BriarSidebar(sidebarViewModel) + BriarSidebar( + viewModel.account.value, + viewModel.uiMode.value, + viewModel::setUiMode + ) VerticalDivider() - when (sidebarViewModel.uiMode.value) { - UiMode.CONTACTS -> PrivateMessageView( - contactListViewModel, - conversationViewModel, - addContactViewModel, - introductionViewModel - ) + when (viewModel.uiMode.value) { + UiMode.CONTACTS -> PrivateMessageScreen() UiMode.SETTINGS -> PlaceHolderSettingsView(isDark, setDark) else -> UiPlaceholder() } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarEventListenerViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarEventListenerViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..7c7b2908fd97c9d4e399959c0d2bf21874f4ac00 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarEventListenerViewModel.kt @@ -0,0 +1,17 @@ +package org.briarproject.briar.desktop.viewmodel + +import org.briarproject.bramble.api.event.EventBus +import org.briarproject.bramble.api.event.EventListener + +abstract class BriarEventListenerViewModel( + private val eventBus: EventBus +) : ViewModel, EventListener { + + override fun onInit() { + eventBus.addListener(this) + } + + override fun onCleared() { + eventBus.removeListener(this) + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..4957735fe98f0868dac97b401bcb1ee8d4395567 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt @@ -0,0 +1,65 @@ +package org.briarproject.briar.desktop.viewmodel + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import org.briarproject.briar.desktop.ui.LocalViewModelProvider +import kotlin.reflect.KClass + +/** + * Returns an existing [ViewModel] or creates a new one + * + * The [ViewModel] is created and retained in the given [viewModelProvider], + * that defaults to [LocalViewModelProvider]. + * 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. + * + * @param viewModelProvider The scope that the created [ViewModel] should be associated with. + * @param key The key to use to identify the [ViewModel]. + * @return A [ViewModel] that is an instance of the given [VM] type. + */ +@Composable +inline fun <reified VM : ViewModel> viewModel( + key: String? = null, + viewModelProvider: ViewModelProvider = checkNotNull(LocalViewModelProvider.current) { + "No ViewModelProvider was provided via LocalViewModelProvider" + } +): VM = viewModel(VM::class, key, viewModelProvider) + +/** + * Returns an existing [ViewModel] or creates a new one + * + * The [ViewModel] is created and retained in the given [viewModelProvider], + * that defaults to [LocalViewModelProvider]. + * 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. + * + * @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. + * @param key The key to use to identify the [ViewModel]. + * @return A [ViewModel] that is an instance of the given [VM] type. + */ +@Composable +fun <VM : ViewModel> viewModel( + modelClass: KClass<VM>, + key: String? = null, + viewModelProvider: ViewModelProvider = checkNotNull(LocalViewModelProvider.current) { + "No ViewModelProvider was provided via LocalViewModelProvider" + } +): VM { + val viewModel = if (key == null) { + viewModelProvider.get(modelClass) + } else { + viewModelProvider.get(key, modelClass) + } + + DisposableEffect(key) { + viewModel.onInit() + + onDispose { + viewModel.onCleared() + } + } + + return viewModel +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..c5934859cd9f35ae843df0be0cf6d2eb57bd2794 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt @@ -0,0 +1,16 @@ +package org.briarproject.briar.desktop.viewmodel + +interface ViewModel { + + /** + * Called to initialize the [ViewModel] as soon as it is first used + * inside a Composable function. + */ + fun onInit() {} + + /** + * Called to clear the [ViewModel] as soon as the calling + * Composable function goes out of scope. + */ + fun onCleared() {} +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelFactory.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3f519db25805ae5da80f0dafb73bfb0604170ab --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelFactory.kt @@ -0,0 +1,27 @@ +package org.briarproject.briar.desktop.viewmodel + +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton +import kotlin.reflect.KClass + +@Singleton +class ViewModelFactory +@Inject +constructor( + private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>> +) { + fun <VM : ViewModel> create(modelClass: KClass<VM>): VM { + var creator = creators[modelClass.java] + if (creator == null) { + for ((key, value) in creators) { + if (modelClass.java.isAssignableFrom(key)) { + creator = value + break + } + } + } + requireNotNull(creator) { "unknown model class $modelClass" } + return creator.get() as VM + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..a1edb2acb4a579258a2691fd054505ebac214ab4 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt @@ -0,0 +1,55 @@ +package org.briarproject.briar.desktop.viewmodel + +import dagger.Binds +import dagger.MapKey +import dagger.Module +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.introduction.IntroductionViewModel +import org.briarproject.briar.desktop.login.LoginViewModel +import org.briarproject.briar.desktop.login.RegistrationViewModel +import org.briarproject.briar.desktop.navigation.SidebarViewModel +import kotlin.reflect.KClass + +@Module +abstract class ViewModelModule { + @MapKey + internal annotation class ViewModelKey(val value: KClass<out ViewModel>) + + @Binds + @IntoMap + @ViewModelKey(LoginViewModel::class) + abstract fun bindLoginViewModel(loginViewModel: LoginViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RegistrationViewModel::class) + abstract fun bindRegistrationViewModel(registrationViewModel: RegistrationViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(SidebarViewModel::class) + abstract fun bindSidebarViewModel(sidebarViewModel: SidebarViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ContactListViewModel::class) + abstract fun bindContactListViewModel(contactListViewModel: ContactListViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(AddContactViewModel::class) + abstract fun bindAddContactViewModel(addContactViewModel: AddContactViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ConversationViewModel::class) + abstract fun bindConversationViewModel(conversationViewModel: ConversationViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(IntroductionViewModel::class) + abstract fun bindIntroductionViewModel(introductionViewModel: IntroductionViewModel): ViewModel +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelProvider.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..83f59d104de4d1e75b10cb282d6349e0834436d6 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelProvider.kt @@ -0,0 +1,34 @@ +package org.briarproject.briar.desktop.viewmodel + +import javax.inject.Inject +import kotlin.reflect.KClass + +class ViewModelProvider +@Inject +constructor( + private val viewModelFactory: ViewModelFactory +) { + + private val viewModels = HashMap<String, ViewModel>() + + fun <VM : ViewModel> get(modelClass: KClass<VM>): VM = + get(modelClass.qualifiedName!!, modelClass) + + fun <VM : ViewModel> get(key: String, modelClass: KClass<VM>): VM { + val viewModel = viewModels[key] + + if (modelClass.isInstance(viewModel)) { + return viewModel as VM + } + + try { + val viewModel = viewModelFactory.create(modelClass) + viewModels[key] = viewModel + return viewModel + } catch (e: InstantiationException) { + throw RuntimeException("Cannot create an instance of $modelClass", e) + } catch (e: IllegalAccessException) { + throw RuntimeException("Cannot create an instance of $modelClass", e) + } + } +} diff --git a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt index 7bc4eff5f61ea7c5e39e8f8563dffdd00489e8fe..cad4b989717dd9430d352a158b39542f4a37ad97 100644 --- a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt +++ b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt @@ -32,6 +32,7 @@ import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl import org.briarproject.briar.desktop.ui.BriarUi import org.briarproject.briar.desktop.ui.BriarUiImpl +import org.briarproject.briar.desktop.viewmodel.ViewModelModule import org.briarproject.briar.test.TestModule import java.io.File import java.nio.file.Path @@ -52,6 +53,7 @@ import javax.inject.Singleton JavaSystemModule::class, SocksModule::class, TestModule::class, + ViewModelModule::class ] ) internal class DesktopTestModule(