diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsViewModel.kt index 3b8b043b71e0ddcd8b42ebd4e2a5aca9a64aa2fa..3ae485767942513d04bd7c51f332bba2d6b2c2f8 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsViewModel.kt @@ -30,15 +30,20 @@ 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.forum.ForumManager +import org.briarproject.briar.client.MessageTreeImpl import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.utils.clearAndAddAll import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel import org.briarproject.briar.desktop.viewmodel.asState import javax.inject.Inject -class ForumsViewModel -@Inject -constructor( +sealed class PostsState +object Loading : PostsState() +class Loaded(private val messageTree: MessageTreeImpl<ThreadItem>) : PostsState() { + val posts: MutableList<ThreadItem> get() = messageTree.depthFirstOrder() +} + +class ForumsViewModel @Inject constructor( private val forumManager: ForumManager, briarExecutors: BriarExecutors, lifecycleManager: LifecycleManager, @@ -60,6 +65,9 @@ constructor( private val _filterBy = mutableStateOf("") val filterBy = _filterBy.asState() + private val _posts = mutableStateOf<PostsState>(Loading) + val posts: State<PostsState> = _posts + override fun onInit() { super.onInit() loadGroups() @@ -96,10 +104,24 @@ constructor( fun selectGroup(groupItem: GroupItem) { _selectedGroupItem.value = groupItem + loadPosts(groupItem.id) } fun isSelected(groupId: GroupId) = _selectedGroupItem.value?.id == groupId + 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) + } + } + } + fun setFilterBy(filter: String) { _filterBy.value = filter } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt index 1be3918079a1548a2f1e8d03329a27ed60d0f759..47ea90732eab3f4c404ad4c6ccd6696a3d8473b0 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt @@ -53,7 +53,6 @@ import org.briarproject.briar.desktop.contact.ContactDropDown.State.CLOSED import org.briarproject.briar.desktop.contact.ContactDropDown.State.MAIN import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider -import org.briarproject.briar.desktop.ui.UiPlaceholder import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.viewmodel.viewModel @@ -67,8 +66,7 @@ fun GroupConversationScreen( GroupConversationHeader(groupItem) { viewModel.deleteGroup(groupItem) } }, content = { padding -> - // Loader() - UiPlaceholder() + ThreadedConversationScreen(viewModel.posts.value) }, ) } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd1dc746ed71c64ae6408b379129eec083d693a3 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadItem.kt @@ -0,0 +1,91 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 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 org.briarproject.bramble.api.identity.Author +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.briar.api.client.MessageTree +import org.briarproject.briar.api.forum.ForumPostHeader +import org.briarproject.briar.api.identity.AuthorInfo +import org.briarproject.briar.desktop.utils.UiUtils.getContactDisplayName +import javax.annotation.concurrent.NotThreadSafe + +@NotThreadSafe +class ForumPostItem(h: ForumPostHeader, text: String?) : ThreadItem( + messageId = h.id, + parentId = h.parentId, + text = text ?: "", + timestamp = h.timestamp, + author = h.author, + authorInfo = h.authorInfo, + isRead = h.isRead +) + +@NotThreadSafe +abstract class ThreadItem( + private val messageId: MessageId, + private val parentId: MessageId?, + val text: String, + private val timestamp: Long, + val author: Author, + val authorInfo: AuthorInfo, + var isRead: Boolean, +) : MessageTree.MessageNode { + + companion object { + const val UNDEFINED = -1 + } + + private var level: Int = UNDEFINED + var isHighlighted = false + + fun getLevel(): Int { + return level + } + + override fun getId(): MessageId { + return messageId + } + + override fun getParentId(): MessageId? { + return parentId + } + + override fun getTimestamp(): Long { + return timestamp + } + + /** + * Returns the author's name, with an alias if one exists. + */ + val authorName: String + get() = getContactDisplayName(author.name, authorInfo.alias) + + override fun setLevel(level: Int) { + this.level = level + } + + override fun hashCode(): Int { + return messageId.hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is ThreadItem && messageId == other.messageId + } +} 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/forums/ThreadedConversationScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..770e1ec6f5a2d1a73a886f2082abea88735ac020 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationScreen.kt @@ -0,0 +1,76 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 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.foundation.VerticalScrollbar +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterEnd +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.briarproject.briar.desktop.forums.ThreadItem.Companion.UNDEFINED +import org.briarproject.briar.desktop.ui.HorizontalDivider +import org.briarproject.briar.desktop.ui.Loader + +@Composable +fun ThreadedConversationScreen( + postsState: PostsState, +) = when (postsState) { + Loading -> Loader() + is Loaded -> { + val scrollState = rememberLazyListState() + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = scrollState, + modifier = Modifier.selectableGroup() + ) { + items(postsState.posts) { item -> + ThreadItemComposable(item) + HorizontalDivider() + } + } + VerticalScrollbar( + adapter = rememberScrollbarAdapter(scrollState), + modifier = Modifier.align(CenterEnd).fillMaxHeight() + ) + } + } +} + +@Composable +fun ThreadItemComposable(item: ThreadItem) { + Text( + text = item.text ?: "", + modifier = Modifier + .padding(4.dp) + .padding( + start = 4.dp + + if (item.getLevel() == UNDEFINED) 0.dp else (item.getLevel() * 8).dp + ), + ) +} diff --git a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/TestRandomConversations.kt b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/TestRandomConversations.kt index 652321be9ce5ca56edb62175beac72d321cb75ed..d832d410a2aff49ba483f2a64481867a4fbbef04 100644 --- a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/TestRandomConversations.kt +++ b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/TestRandomConversations.kt @@ -19,5 +19,5 @@ package org.briarproject.briar.desktop fun main() = RunWithTemporaryAccount { - getTestDataCreator().createTestData(5, 20, 50, 4, 10, 10) + getTestDataCreator().createTestData(5, 20, 50, 4, 10, 42) }.run()