diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumListViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumListViewModel.kt index 5efeec62135b6c776247089183b0f3cc15217fdf..f73e7dd194e9ec3345382b9229d99dd6c71eaeda 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumListViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumListViewModel.kt @@ -30,6 +30,7 @@ import org.briarproject.bramble.api.sync.GroupId import org.briarproject.briar.api.client.PostHeader import org.briarproject.briar.api.forum.ForumManager import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent +import org.briarproject.briar.desktop.forums.conversation.ForumConversationViewModel import org.briarproject.briar.desktop.group.GroupListViewModel import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.utils.removeFirst @@ -39,7 +40,7 @@ import javax.inject.Inject class ForumListViewModel @Inject constructor( private val forumManager: ForumManager, - threadViewModel: ThreadedConversationViewModel, + threadViewModel: ForumConversationViewModel, briarExecutors: BriarExecutors, lifecycleManager: LifecycleManager, db: TransactionManager, @@ -85,7 +86,7 @@ class ForumListViewModel } override fun addOwnMessage(header: PostHeader) { - selectedGroupId.value?.let { id -> updateItem(id) { it.updateOnPostReceived(header) } } + // no-op since GroupMessageAddedEvent is also sent on locally added message } private fun updateItem(groupId: GroupId, update: (ForumItem) -> ForumItem) = diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumStrings.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumStrings.kt index 401007e7de157bd241b64996be529648852dd0c6..b9d7413254813505a206ccae442d67a02d2ef2d2 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumStrings.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumStrings.kt @@ -47,6 +47,8 @@ object ForumStrings : GroupStrings( sharedWith = { total, online -> i18nF("forum.sharing.status.with", total, online) }, + unreadJumpToPrevious = i18n("access.forums.jump_to_prev_unread"), + unreadJumpToNext = i18n("access.forums.jump_to_next_unread"), deleteDialogTitle = i18n("forum.delete.dialog.title"), deleteDialogMessage = i18n("forum.delete.dialog.message"), deleteDialogButton = i18n("forum.delete.dialog.button"), 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 deleted file mode 100644 index 85168f8a8eabfcef4f5638faf001a29e371f35aa..0000000000000000000000000000000000000000 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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.forums - -import androidx.compose.runtime.mutableStateOf -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import mu.KotlinLogging -import org.briarproject.bramble.api.crypto.CryptoExecutor -import org.briarproject.bramble.api.db.TransactionManager -import org.briarproject.bramble.api.event.Event -import org.briarproject.bramble.api.event.EventBus -import org.briarproject.bramble.api.identity.IdentityManager -import org.briarproject.bramble.api.identity.LocalAuthor -import org.briarproject.bramble.api.lifecycle.LifecycleManager -import org.briarproject.bramble.api.sync.GroupId -import org.briarproject.bramble.api.sync.MessageId -import org.briarproject.bramble.api.system.Clock -import org.briarproject.briar.api.client.MessageTracker -import org.briarproject.briar.api.forum.ForumManager -import org.briarproject.briar.api.forum.ForumPostHeader -import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent -import org.briarproject.briar.client.MessageTreeImpl -import org.briarproject.briar.desktop.forums.conversation.ForumPostItem -import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel -import org.briarproject.briar.desktop.group.GroupItem -import org.briarproject.briar.desktop.group.conversation.ThreadItem -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 -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, - @CryptoExecutor private val cryptoDispatcher: CoroutineDispatcher, - briarExecutors: BriarExecutors, - lifecycleManager: LifecycleManager, - db: TransactionManager, - private val eventBus: EventBus, -) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) { - - companion object { - private val LOG = KotlinLogging.logger {} - } - - private val _groupItem = mutableStateOf<GroupItem?>(null) - val groupItem = _groupItem.asState() - - private lateinit var onPostAdded: (header: ForumPostHeader) -> Unit - - private val _posts = mutableStateOf<PostsState>(Loading) - val posts = _posts.asState() - - private val _selectedPost = mutableStateOf<ThreadItem?>(null) - val selectedPost = _selectedPost.asState() - - @UiExecutor - fun setGroupItem(groupItem: GroupItem, onPostAdded: (header: ForumPostHeader) -> Unit) { - this.onPostAdded = onPostAdded - _groupItem.value = groupItem - _selectedPost.value = null - forumSharingViewModel.setGroupId(groupItem.id) - loadPosts(groupItem.id) - } - - @UiExecutor - override fun eventOccurred(e: Event) { - if (e is ForumPostReceivedEvent) { - if (e.groupId == _groupItem.value?.id) { - val item = ForumPostItem(e.header, e.text) - addItem(item, null) - } - } - } - - 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 -> - val items = forumManager.getPostHeaders(txn, groupId).map { header -> - ForumPostItem(header, forumManager.getPostText(txn, header.id)) - } - val tree = MessageTreeImpl<ThreadItem>().apply { add(items) } - txn.attach { - _posts.value = Loaded(tree) - } - } - } - - @UiExecutor - fun selectPost(post: ThreadItem?) { - _selectedPost.value = post - if (post != null && !post.isRead) markPostRead(post.id) - } - - @UiExecutor - @OptIn(DelicateCoroutinesApi::class) - fun createPost(text: String) = GlobalScope.launch { - val groupId = _groupItem.value?.id ?: return@launch - val parentId = _selectedPost.value?.id - val author = runOnDbThreadWithTransaction<LocalAuthor>(false) { txn -> - identityManager.getLocalAuthor(txn) - } - val count = runOnDbThreadWithTransaction<MessageTracker.GroupCount>(false) { txn -> - forumManager.getGroupCount(txn, groupId) - } - val timestamp = max(count.latestMsgTime + 1, clock.currentTimeMillis()) - val post = withContext(cryptoDispatcher) { - forumManager.createLocalPost(groupId, text, timestamp, parentId, author) - } - runOnDbThreadWithTransaction(false) { txn -> - val header = forumManager.addLocalPost(txn, post) - txn.attach { - val item = ForumPostItem(header, text) - addItem(item, item.id) - onPostAdded(header) - // unselect post that we just replied to - if (parentId != null) { - _selectedPost.value = null - } - } - } - } - - @UiExecutor - private fun addItem(item: ThreadItem, scrollTo: MessageId? = null) { - // If items haven't loaded, we need to wait until they have. - // Since this was a R/W DB transaction, the load will pick up this item. - val tree = (posts.value as? Loaded)?.messageTree ?: return - tree.add(item) - _posts.value = Loaded(tree, scrollTo) - } - - @UiExecutor - private fun markPostRead(id: MessageId) = markPostsRead(listOf(id)) - - @UiExecutor - fun markPostsRead(ids: List<MessageId>) { - // TODO messageTree.get(id) would be nice, but not in briar-core - val readIds = (posts.value as? Loaded)?.posts?.filter { item -> - !item.isRead && ids.contains(item.id) - }?.map { item -> - item.isRead = true - item.id - } ?: emptyList() - - val groupId = _groupItem.value?.id - if (readIds.isNotEmpty() && groupId != null) { - runOnDbThread { - readIds.forEach { id -> - forumManager.setReadFlag(groupId, id, true) - } - } - // we don't attach this to the transaction that actually changes the DB, - // but that should be fine for this purpose of just decrementing a counter - eventBus.broadcast(ForumPostReadEvent(groupId, readIds.size)) - // TODO replace immutable ThreadItems instead to avoid recomposing whole list - val messageTree = (posts.value as? Loaded)?.messageTree ?: return - _posts.value = Loaded(messageTree) - } - } - - fun deleteGroup() { - _groupItem.value?.let { forumManager.removeForum((it as ForumItem).forum) } - } -} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/conversation/ForumConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/conversation/ForumConversationViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..f3d19e999fdcaf90a9809d19a0393531ddbe53e4 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/conversation/ForumConversationViewModel.kt @@ -0,0 +1,123 @@ +/* + * 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.forums.conversation + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mu.KotlinLogging +import org.briarproject.bramble.api.crypto.CryptoExecutor +import org.briarproject.bramble.api.db.DatabaseExecutor +import org.briarproject.bramble.api.db.Transaction +import org.briarproject.bramble.api.db.TransactionManager +import org.briarproject.bramble.api.event.Event +import org.briarproject.bramble.api.event.EventBus +import org.briarproject.bramble.api.identity.IdentityManager +import org.briarproject.bramble.api.identity.LocalAuthor +import org.briarproject.bramble.api.lifecycle.LifecycleManager +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.bramble.api.system.Clock +import org.briarproject.briar.api.client.MessageTracker +import org.briarproject.briar.api.forum.ForumManager +import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent +import org.briarproject.briar.desktop.forums.ForumItem +import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel +import org.briarproject.briar.desktop.group.conversation.ThreadedConversationViewModel +import org.briarproject.briar.desktop.threading.BriarExecutors +import org.briarproject.briar.desktop.threading.UiExecutor +import java.lang.Long.max +import javax.inject.Inject + +class ForumConversationViewModel @Inject constructor( + forumSharingViewModel: ForumSharingViewModel, + private val forumManager: ForumManager, + private val identityManager: IdentityManager, + private val clock: Clock, + @CryptoExecutor private val cryptoDispatcher: CoroutineDispatcher, + briarExecutors: BriarExecutors, + lifecycleManager: LifecycleManager, + db: TransactionManager, + eventBus: EventBus, +) : ThreadedConversationViewModel( + forumSharingViewModel, + briarExecutors, + lifecycleManager, + db, + eventBus +) { + + companion object { + private val LOG = KotlinLogging.logger {} + } + + @UiExecutor + override fun eventOccurred(e: Event) { + if (e is ForumPostReceivedEvent) { + if (e.groupId == groupItem.value?.id) { + val item = ForumPostItem(e.header, e.text) + addItem(item, null) + } + } + } + + override fun loadThreadItems(txn: Transaction, groupId: GroupId) = + forumManager.getPostHeaders(txn, groupId).map { header -> + ForumPostItem(header, forumManager.getPostText(txn, header.id)) + } + + @UiExecutor + @OptIn(DelicateCoroutinesApi::class) + override fun createThreadItem(text: String) = GlobalScope.launch { + val groupId = groupItem.value?.id ?: return@launch + val parentId = selectedThreadItem.value?.id + val author = runOnDbThreadWithTransaction<LocalAuthor>(false) { txn -> + identityManager.getLocalAuthor(txn) + } + val count = runOnDbThreadWithTransaction<MessageTracker.GroupCount>(false) { txn -> + forumManager.getGroupCount(txn, groupId) + } + val timestamp = max(count.latestMsgTime + 1, clock.currentTimeMillis()) + val post = withContext(cryptoDispatcher) { + forumManager.createLocalPost(groupId, text, timestamp, parentId, author) + } + runOnDbThreadWithTransaction(false) { txn -> + val header = forumManager.addLocalPost(txn, post) + txn.attach { + val item = ForumPostItem(header, text) + addItem(item, item.id) + onThreadItemAdded(header) + // unselect post that we just replied to + if (parentId != null) { + selectThreadItem(null) + } + } + } + } + + @DatabaseExecutor + override fun markThreadItemRead(groupId: GroupId, id: MessageId) = + forumManager.setReadFlag(groupId, id, true) + + override fun deleteGroup() { + groupItem.value?.let { forumManager.removeForum((it as ForumItem).forum) } + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupListViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupListViewModel.kt index 4c01f085ab222e11040637fae0c2a7a797bdab90..19ba8c0b8e067a1d786d33690718741395f1cd64 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupListViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupListViewModel.kt @@ -32,7 +32,7 @@ import org.briarproject.bramble.api.sync.GroupId import org.briarproject.bramble.api.sync.event.GroupAddedEvent import org.briarproject.bramble.api.sync.event.GroupRemovedEvent import org.briarproject.briar.api.client.PostHeader -import org.briarproject.briar.desktop.forums.ThreadedConversationViewModel +import org.briarproject.briar.desktop.group.conversation.ThreadedConversationViewModel import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.utils.clearAndAddAll import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupStrings.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupStrings.kt index 1c7a45f8bc8c62fd8f93f582d6c7950032b54807..3ed1451d5696b07a3e7da58371170b86e13bd053 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupStrings.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupStrings.kt @@ -32,6 +32,8 @@ abstract class GroupStrings( val lastMessage: (String) -> String, val groupNameMaxLength: Int, val sharedWith: (total: Int, online: Int) -> String, + val unreadJumpToPrevious: String, + val unreadJumpToNext: String, // todo: will need to be different for private groups depending on creator or not val deleteDialogTitle: String, val deleteDialogMessage: String, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/GroupConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/GroupConversationScreen.kt index e1e1c1fc5f9764cd714696b8fcd1a3556d10439f..c6708c2bbc3e5d8cfe6d242429eb3e0762b58f69 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/GroupConversationScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/GroupConversationScreen.kt @@ -51,8 +51,6 @@ 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.forums.ThreadedConversationScreen -import org.briarproject.briar.desktop.forums.ThreadedConversationViewModel import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel import org.briarproject.briar.desktop.group.GroupCircle import org.briarproject.briar.desktop.group.GroupInputComposable @@ -82,17 +80,18 @@ fun GroupConversationScreen( }, content = { padding -> ThreadedConversationScreen( - postsState = viewModel.posts.value, - selectedPost = viewModel.selectedPost.value, - onPostSelected = viewModel::selectPost, - onPostsVisible = viewModel::markPostsRead, + strings = strings, + state = viewModel.state.value, + selectedThreadItem = viewModel.selectedThreadItem.value, + onThreadItemSelected = viewModel::selectThreadItem, + onThreadItemsVisible = viewModel::markThreadItemsRead, modifier = Modifier.padding(padding) ) }, bottomBar = { - val onCloseReply = { viewModel.selectPost(null) } - GroupInputComposable(viewModel.selectedPost.value, onCloseReply) { text -> - viewModel.createPost(text) + val onCloseReply = { viewModel.selectThreadItem(null) } + GroupInputComposable(viewModel.selectedThreadItem.value, onCloseReply) { text -> + viewModel.createThreadItem(text) } } ) diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationScreen.kt similarity index 78% rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationScreen.kt rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationScreen.kt index bb2e1db993adb516935d3edc665c5395c5000c0d..d1fc5090fadfc820cde9320fe9946fcdca95764f 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationScreen.kt @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -package org.briarproject.briar.desktop.forums +package org.briarproject.briar.desktop.group.conversation import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.layout.Box @@ -36,26 +36,25 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import org.briarproject.bramble.api.sync.MessageId import org.briarproject.briar.desktop.conversation.reallyVisibleItemsInfo -import org.briarproject.briar.desktop.group.conversation.ThreadItem -import org.briarproject.briar.desktop.group.conversation.ThreadItemView -import org.briarproject.briar.desktop.group.conversation.getMaxNestingLevel +import org.briarproject.briar.desktop.group.GroupStrings import org.briarproject.briar.desktop.ui.Loader import org.briarproject.briar.desktop.ui.isWindowFocused @Composable fun ThreadedConversationScreen( - postsState: PostsState, - selectedPost: ThreadItem?, - onPostSelected: (ThreadItem) -> Unit, - onPostsVisible: (List<MessageId>) -> Unit, + strings: GroupStrings, + state: ThreadedConversationScreenState, + selectedThreadItem: ThreadItem?, + onThreadItemSelected: (ThreadItem) -> Unit, + onThreadItemsVisible: (List<MessageId>) -> Unit, modifier: Modifier = Modifier, -) = when (postsState) { +) = when (state) { Loading -> Loader() is Loaded -> { val scrollState = rememberLazyListState() // scroll to item if needed - if (postsState.scrollTo != null) LaunchedEffect(postsState) { - val index = postsState.posts.indexOfFirst { it.id == postsState.scrollTo } + if (state.scrollTo != null) LaunchedEffect(state) { + val index = state.posts.indexOfFirst { it.id == state.scrollTo } if (index != -1) scrollState.scrollToItem(index, -50) } val maxNestingLevel = getMaxNestingLevel() @@ -64,16 +63,16 @@ fun ThreadedConversationScreen( state = scrollState, modifier = Modifier.padding(end = 8.dp).selectableGroup() ) { - items(postsState.posts, key = { item -> item.id }) { item -> + items(state.posts, key = { item -> item.id }) { item -> ThreadItemView( item = item, maxNestingLevel = maxNestingLevel, - selectedPost = selectedPost, - onPostSelected = onPostSelected, + selectedPost = selectedThreadItem, + onPostSelected = onThreadItemSelected, ) } } - UnreadFabs(scrollState, postsState) + UnreadFabs(strings, scrollState, state) VerticalScrollbar( adapter = rememberScrollbarAdapter(scrollState), modifier = Modifier.align(CenterEnd).fillMaxHeight() @@ -82,7 +81,7 @@ fun ThreadedConversationScreen( // if Briar Desktop currently has focus, // mark all posts visible on the screen as read after some delay LaunchedEffect( - postsState, + state, scrollState.firstVisibleItemIndex, scrollState.firstVisibleItemScrollOffset ) { @@ -90,7 +89,7 @@ fun ThreadedConversationScreen( val visibleMessageIds = scrollState.layoutInfo.reallyVisibleItemsInfo.map { it.key as MessageId } - onPostsVisible(visibleMessageIds) + onThreadItemsVisible(visibleMessageIds) } } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/PostsState.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationScreenState.kt similarity index 91% rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/PostsState.kt rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationScreenState.kt index d3fca2ceb66b1bff694b72a1951a09ad32a45ad2..1f2f870a7b0693af5f5eb1885206f0d552685c19 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/PostsState.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationScreenState.kt @@ -16,19 +16,18 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -package org.briarproject.briar.desktop.forums +package org.briarproject.briar.desktop.group.conversation import org.briarproject.bramble.api.sync.MessageId import org.briarproject.briar.client.MessageTreeImpl -import org.briarproject.briar.desktop.group.conversation.ThreadItem import org.briarproject.briar.desktop.threading.UiExecutor -sealed class PostsState -object Loading : PostsState() +sealed class ThreadedConversationScreenState +object Loading : ThreadedConversationScreenState() class Loaded( val messageTree: MessageTreeImpl<ThreadItem>, val scrollTo: MessageId? = null, -) : PostsState() { +) : ThreadedConversationScreenState() { val posts: List<ThreadItem> = messageTree.depthFirstOrder() @UiExecutor diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bbe194cf197776a5e4549f09f1b7ce175245502 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/ThreadedConversationViewModel.kt @@ -0,0 +1,144 @@ +/* + * 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.group.conversation + +import androidx.compose.runtime.mutableStateOf +import kotlinx.coroutines.Job +import mu.KotlinLogging +import org.briarproject.bramble.api.db.DatabaseExecutor +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.bramble.api.sync.MessageId +import org.briarproject.briar.api.client.PostHeader +import org.briarproject.briar.client.MessageTreeImpl +import org.briarproject.briar.desktop.forums.ForumPostReadEvent +import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel +import org.briarproject.briar.desktop.group.GroupItem +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, + briarExecutors: BriarExecutors, + lifecycleManager: LifecycleManager, + db: TransactionManager, + private val eventBus: EventBus, +) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) { + + companion object { + private val LOG = KotlinLogging.logger {} + } + + private val _groupItem = mutableStateOf<GroupItem?>(null) + val groupItem = _groupItem.asState() + + protected lateinit var onThreadItemAdded: (header: PostHeader) -> Unit + + private val _state = mutableStateOf<ThreadedConversationScreenState>(Loading) + val state = _state.asState() + + private val _selectedThreadItem = mutableStateOf<ThreadItem?>(null) + val selectedThreadItem = _selectedThreadItem.asState() + + @UiExecutor + fun setGroupItem(groupItem: GroupItem, onThreadItemAdded: (header: PostHeader) -> Unit) { + this.onThreadItemAdded = onThreadItemAdded + _groupItem.value = groupItem + _selectedThreadItem.value = null + forumSharingViewModel.setGroupId(groupItem.id) + loadThreadItems(groupItem.id) + } + + override fun onInit() { + super.onInit() + forumSharingViewModel.onEnterComposition() + } + + override fun onCleared() { + super.onCleared() + forumSharingViewModel.onExitComposition() + } + + protected abstract fun loadThreadItems(txn: Transaction, groupId: GroupId): List<ThreadItem> + + private fun loadThreadItems(groupId: GroupId) { + _state.value = Loading + runOnDbThreadWithTransaction(true) { txn -> + val items = loadThreadItems(txn, groupId) + val tree = MessageTreeImpl<ThreadItem>().apply { add(items) } + txn.attach { + _state.value = Loaded(tree) + } + } + } + + @UiExecutor + fun selectThreadItem(item: ThreadItem?) { + _selectedThreadItem.value = item + if (item != null && !item.isRead) markThreadItemsRead(listOf(item.id)) + } + + @UiExecutor + abstract fun createThreadItem(text: String): Job + + @UiExecutor + protected fun addItem(item: ThreadItem, scrollTo: MessageId? = null) { + // If items haven't loaded, we need to wait until they have. + // Since this was a R/W DB transaction, the load will pick up this item. + val tree = (state.value as? Loaded)?.messageTree ?: return + tree.add(item) + _state.value = Loaded(tree, scrollTo) + } + + @DatabaseExecutor + abstract fun markThreadItemRead(groupId: GroupId, id: MessageId) + + @UiExecutor + fun markThreadItemsRead(ids: List<MessageId>) { + // TODO messageTree.get(id) would be nice, but not in briar-core + val readIds = (state.value as? Loaded)?.posts?.filter { item -> + !item.isRead && ids.contains(item.id) + }?.map { item -> + item.isRead = true + item.id + } ?: emptyList() + + val groupId = _groupItem.value?.id + if (readIds.isNotEmpty() && groupId != null) { + runOnDbThread { + readIds.forEach { id -> + markThreadItemRead(groupId, id) + } + } + // we don't attach this to the transaction that actually changes the DB, + // but that should be fine for this purpose of just decrementing a counter + eventBus.broadcast(ForumPostReadEvent(groupId, readIds.size)) + // TODO replace immutable ThreadItems instead to avoid recomposing whole list + val messageTree = (state.value as? Loaded)?.messageTree ?: return + _state.value = Loaded(messageTree) + } + } + + abstract fun deleteGroup() +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/UnreadFabs.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/UnreadFabs.kt similarity index 90% rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/UnreadFabs.kt rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/UnreadFabs.kt index d9e4a3025d068d62d0b92d9e1dcf8f53b3416027..b3e1dc37100c10a3ab420e8f87033adb78f6d810 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/UnreadFabs.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/conversation/UnreadFabs.kt @@ -1,6 +1,6 @@ /* * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project + * Copyright (C) 2021-2023 The Briar Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -package org.briarproject.briar.desktop.forums +package org.briarproject.briar.desktop.group.conversation import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box @@ -38,13 +38,13 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.briarproject.briar.desktop.conversation.firstReallyVisibleItemIndex import org.briarproject.briar.desktop.conversation.lastReallyVisibleItemIndex +import org.briarproject.briar.desktop.group.GroupStrings import org.briarproject.briar.desktop.theme.ChevronDown import org.briarproject.briar.desktop.theme.ChevronUp import org.briarproject.briar.desktop.ui.NumberBadge -import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @Composable -fun BoxScope.UnreadFabs(scrollState: LazyListState, postsState: Loaded) { +fun BoxScope.UnreadFabs(strings: GroupStrings, scrollState: LazyListState, postsState: Loaded) { val coroutineScope = rememberCoroutineScope() // remember first really visible item index based on scroll offset @@ -59,10 +59,10 @@ fun BoxScope.UnreadFabs(scrollState: LazyListState, postsState: Loaded) { visible = unreadInfo.numUnread > 0, modifier = Modifier.align(TopEnd).padding(16.dp), ) { - UnreadPostsFab( + UnreadFab( imageVector = Icons.Default.ChevronUp, numUnread = unreadInfo.numUnread, - contentDescription = i18n("access.forums.jump_to_prev_unread"), + contentDescription = strings.unreadJumpToPrevious, onClick = { coroutineScope.launch { if (unreadInfo.nextUnreadIndex != null) { @@ -88,10 +88,10 @@ fun BoxScope.UnreadFabs(scrollState: LazyListState, postsState: Loaded) { visible = bottomUnreadInfo.numUnread > 0, modifier = Modifier.align(BottomEnd).padding(16.dp), ) { - UnreadPostsFab( + UnreadFab( imageVector = Icons.Default.ChevronDown, numUnread = bottomUnreadInfo.numUnread, - contentDescription = i18n("access.forums.jump_to_next_unread"), + contentDescription = strings.unreadJumpToNext, onClick = { coroutineScope.launch { if (bottomUnreadInfo.nextUnreadIndex != null) scrollState.animateScrollToItem( @@ -106,7 +106,7 @@ fun BoxScope.UnreadFabs(scrollState: LazyListState, postsState: Loaded) { } @Composable -fun UnreadPostsFab( +private fun UnreadFab( imageVector: ImageVector, numUnread: Int, contentDescription: String, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupListViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupListViewModel.kt index c6310296e2a4e54c0069876cb7f6e56a39f4bcbf..8f72074eb07e4f8d920476e8fd72318f22467667 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupListViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupListViewModel.kt @@ -34,8 +34,8 @@ import org.briarproject.briar.api.privategroup.GroupMessageFactory import org.briarproject.briar.api.privategroup.PrivateGroupFactory import org.briarproject.briar.api.privategroup.PrivateGroupManager import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent -import org.briarproject.briar.desktop.forums.ThreadedConversationViewModel import org.briarproject.briar.desktop.group.GroupListViewModel +import org.briarproject.briar.desktop.privategroup.conversation.PrivateGroupConversationViewModel import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.utils.removeFirst import org.briarproject.briar.desktop.utils.replaceFirst @@ -48,7 +48,7 @@ class PrivateGroupListViewModel private val privateGroupManager: PrivateGroupManager, private val privateGroupFactory: PrivateGroupFactory, private val privateGroupMessageFactory: GroupMessageFactory, - threadViewModel: ThreadedConversationViewModel, // todo: subclass + threadViewModel: PrivateGroupConversationViewModel, briarExecutors: BriarExecutors, lifecycleManager: LifecycleManager, db: TransactionManager, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupStrings.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupStrings.kt index 6d8415f83943b187e45afaab13e332892ae321fc..dbd8c400683b026566aa83219c0081a2bee4b214 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupStrings.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupStrings.kt @@ -48,6 +48,8 @@ object PrivateGroupStrings : GroupStrings( sharedWith = { total, online -> i18nF("forum.sharing.status.with", total, online) }, + unreadJumpToPrevious = i18n("access.forums.jump_to_prev_unread"), + unreadJumpToNext = i18n("access.forums.jump_to_next_unread"), deleteDialogTitle = i18n("forum.delete.dialog.title"), deleteDialogMessage = i18n("forum.delete.dialog.message"), deleteDialogButton = i18n("forum.delete.dialog.button"), 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 new file mode 100644 index 0000000000000000000000000000000000000000..9ae61f1688a706b8d1e8bc7bf35393eaf675999f --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt @@ -0,0 +1,130 @@ +/* + * 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.conversation + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mu.KotlinLogging +import org.briarproject.bramble.api.crypto.CryptoExecutor +import org.briarproject.bramble.api.db.DatabaseExecutor +import org.briarproject.bramble.api.db.Transaction +import org.briarproject.bramble.api.db.TransactionManager +import org.briarproject.bramble.api.event.Event +import org.briarproject.bramble.api.event.EventBus +import org.briarproject.bramble.api.identity.IdentityManager +import org.briarproject.bramble.api.identity.LocalAuthor +import org.briarproject.bramble.api.lifecycle.LifecycleManager +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.bramble.api.system.Clock +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.forums.sharing.ForumSharingViewModel +import org.briarproject.briar.desktop.group.conversation.ThreadedConversationViewModel +import org.briarproject.briar.desktop.threading.BriarExecutors +import org.briarproject.briar.desktop.threading.UiExecutor +import java.lang.Long.max +import javax.inject.Inject + +class PrivateGroupConversationViewModel @Inject constructor( + forumSharingViewModel: ForumSharingViewModel, + private val privateGroupManager: PrivateGroupManager, + private val privateGroupMessageFactory: GroupMessageFactory, + private val identityManager: IdentityManager, + private val clock: Clock, + @CryptoExecutor private val cryptoDispatcher: CoroutineDispatcher, + briarExecutors: BriarExecutors, + lifecycleManager: LifecycleManager, + db: TransactionManager, + eventBus: EventBus, +) : ThreadedConversationViewModel( + forumSharingViewModel, + briarExecutors, + lifecycleManager, + db, + eventBus +) { + + companion object { + private val LOG = KotlinLogging.logger {} + } + + @UiExecutor + override fun eventOccurred(e: Event) { + // todo + /*if (e is ForumPostReceivedEvent) { + if (e.groupId == groupItem.value?.id) { + val item = ForumPostItem(e.header, e.text) + addItem(item, null) + } + }*/ + } + + override fun loadThreadItems(txn: Transaction, groupId: GroupId) = + privateGroupManager.getHeaders(txn, groupId).map { header -> + PrivateGroupMessageItem( + header, + if (header !is JoinMessageHeader) privateGroupManager.getMessageText(txn, header.id) + else "" // todo + ) + } + + @UiExecutor + @OptIn(DelicateCoroutinesApi::class) + override fun createThreadItem(text: String) = GlobalScope.launch { + val groupId = groupItem.value?.id ?: return@launch + val parentId = selectedThreadItem.value?.id + val author = runOnDbThreadWithTransaction<LocalAuthor>(false) { txn -> + identityManager.getLocalAuthor(txn) + } + val previousMsgId = runOnDbThread<MessageId> { privateGroupManager.getPreviousMsgId(groupId) } + val count = runOnDbThreadWithTransaction<MessageTracker.GroupCount>(false) { txn -> + privateGroupManager.getGroupCount(txn, groupId) + } + val timestamp = max(count.latestMsgTime + 1, clock.currentTimeMillis()) + val post = withContext(cryptoDispatcher) { + privateGroupMessageFactory.createGroupMessage(groupId, timestamp, parentId, author, text, previousMsgId) + } + runOnDbThreadWithTransaction(false) { txn -> + val header = privateGroupManager.addLocalMessage(txn, post) + txn.attach { + val item = PrivateGroupMessageItem(header, text) + addItem(item, item.id) + onThreadItemAdded(header) + // unselect post that we just replied to + if (parentId != null) { + selectThreadItem(null) + } + } + } + } + + @DatabaseExecutor + override fun markThreadItemRead(groupId: GroupId, id: MessageId) = + privateGroupManager.setReadFlag(groupId, id, true) + + override fun deleteGroup() { + groupItem.value?.let { privateGroupManager.removePrivateGroup(it.id) } + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupMessageItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupMessageItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..aeed9c2a948a3cdfacab3a8263847cdb8303a7a3 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupMessageItem.kt @@ -0,0 +1,34 @@ +/* + * 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 org.briarproject.briar.api.privategroup.GroupMessageHeader +import org.briarproject.briar.desktop.group.conversation.ThreadItem +import javax.annotation.concurrent.NotThreadSafe + +@NotThreadSafe +class PrivateGroupMessageItem(h: GroupMessageHeader, text: String?) : ThreadItem( + messageId = h.id, + parentId = h.parentId, + text = text ?: "", + timestamp = h.timestamp, + author = h.author, + authorInfo = h.authorInfo, + isRead = h.isRead +) diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutors.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutors.kt index d1affd5700904b60e0c2a6bb9e6768400ab3f50d..6c5a98c48b796625e8342f4b70da1dc955d52067 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutors.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutors.kt @@ -1,6 +1,6 @@ /* * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project + * Copyright (C) 2021-2023 The Briar Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -25,6 +25,8 @@ import org.briarproject.bramble.api.lifecycle.IoExecutor interface BriarExecutors { fun onDbThread(@DatabaseExecutor task: () -> Unit) + suspend fun <T> runOnDbThread(@DatabaseExecutor task: () -> T): T + fun onDbThreadWithTransaction( readOnly: Boolean, @DatabaseExecutor task: (Transaction) -> Unit, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutorsImpl.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutorsImpl.kt index 28f36dbf3182386c2fbd8adbbca3dfaf9e1a8339..de0923ebec55afdf348890f73d397c8645ec928c 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutorsImpl.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutorsImpl.kt @@ -1,6 +1,6 @@ /* * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project + * Copyright (C) 2021-2023 The Briar Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -57,6 +57,16 @@ constructor( } } + override suspend fun <T> runOnDbThread(@DatabaseExecutor task: () -> T) = + suspendCoroutine { cont -> + // The coroutine suspends until the DatabaseExecutor has finished the task + // and ended the transaction. It then resumes with the returned value. + onDbThread { + val t = task() + cont.resume(t) + } + } + override fun onDbThreadWithTransaction( readOnly: Boolean, @DatabaseExecutor task: (Transaction) -> Unit, @@ -80,7 +90,7 @@ constructor( override suspend fun <T> runOnDbThreadWithTransaction( readOnly: Boolean, - @DatabaseExecutor task: (Transaction) -> T + @DatabaseExecutor task: (Transaction) -> T, ) = suspendCoroutine<T> { cont -> // The coroutine suspends until the DatabaseExecutor has finished the task // and ended the transaction. It then resumes with the returned value. 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 a67eb885d68cfab93e5ca4edd636a8fc5420f901..62a7a7fb4ac14a7aadab92271f46b7140099eeef 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 @@ -1,6 +1,6 @@ /* * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project + * Copyright (C) 2021-2023 The Briar Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -49,6 +49,14 @@ abstract class DbViewModel( task: (Transaction) -> Unit, ) = briarExecutors.onDbThreadWithTransaction(readOnly, task) + /** + * Waits for the DB to open and runs the given [task] on the [DatabaseExecutor], + * returning its result. + */ + protected suspend fun <T> runOnDbThread( + @DatabaseExecutor task: () -> T, + ): T = briarExecutors.runOnDbThread(task) + /** * Waits for the DB to open and runs the given [task] on the [DatabaseExecutor], * returning its result.